Completion command for bash
- Add basic support of defining a command `completion bash` which outputs a generic bash script which can be used in a user environment. - Idea for completion is copied from go's cobra library what comes for a bash dance itself. - Goes through command registry, builds a model for command structure and uses antlr st4 for templating bash. - Should give foundation to create other completions just like in cobra. - Currently as we don't know a root-command in a generic way, option `spring.shell.command.completion.root-command` is required user to set. - Fixes #343
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -24,6 +24,7 @@
|
||||
<properties>
|
||||
<jline.version>3.21.0</jline.version>
|
||||
<jcommander.version>1.81</jcommander.version>
|
||||
<antlr-st4.version>4.3.1</antlr-st4.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@@ -98,6 +99,11 @@
|
||||
<version>${jcommander.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>ST4</artifactId>
|
||||
<version>${antlr-st4.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -179,6 +179,28 @@ public class SpringShellProperties {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CompletionCommand {
|
||||
|
||||
private boolean enabled = true;
|
||||
private String rootCommand;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getRootCommand() {
|
||||
return rootCommand;
|
||||
}
|
||||
|
||||
public void setRootCommand(String rootCommand) {
|
||||
this.rootCommand = rootCommand;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Command {
|
||||
|
||||
private HelpCommand help = new HelpCommand();
|
||||
@@ -187,6 +209,7 @@ public class SpringShellProperties {
|
||||
private StacktraceCommand stacktrace = new StacktraceCommand();
|
||||
private ScriptCommand script = new ScriptCommand();
|
||||
private HistoryCommand history = new HistoryCommand();
|
||||
private CompletionCommand completion = new CompletionCommand();
|
||||
|
||||
public void setHelp(HelpCommand help) {
|
||||
this.help = help;
|
||||
@@ -235,5 +258,13 @@ public class SpringShellProperties {
|
||||
public void setHistory(HistoryCommand history) {
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
public CompletionCommand getCompletion() {
|
||||
return completion;
|
||||
}
|
||||
|
||||
public void setCompletion(CompletionCommand completion) {
|
||||
this.completion = completion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -22,10 +22,14 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.boot.condition.OnCompletionCommandCondition;
|
||||
import org.springframework.shell.result.ThrowableResultHandler;
|
||||
import org.springframework.shell.standard.commands.Clear;
|
||||
import org.springframework.shell.standard.commands.Completion;
|
||||
import org.springframework.shell.standard.commands.Help;
|
||||
import org.springframework.shell.standard.commands.History;
|
||||
import org.springframework.shell.standard.commands.Quit;
|
||||
@@ -39,6 +43,7 @@ import org.springframework.shell.standard.commands.Stacktrace;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ Help.Command.class })
|
||||
@EnableConfigurationProperties(SpringShellProperties.class)
|
||||
public class StandardCommandsAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -82,4 +87,11 @@ public class StandardCommandsAutoConfiguration {
|
||||
public History historyCommand(org.jline.reader.History jLineHistory) {
|
||||
return new History(jLineHistory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Completion.Command.class)
|
||||
@Conditional(OnCompletionCommandCondition.class)
|
||||
public Completion completion(SpringShellProperties properties) {
|
||||
return new Completion(properties.getCommand().getCompletion().getRootCommand());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2022 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.boot.condition;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
public class OnCompletionCommandCondition extends AllNestedConditions {
|
||||
|
||||
public OnCompletionCommandCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(prefix = "spring.shell.command.completion", value = "root-command")
|
||||
static class RootNameCondition {
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(prefix = "spring.shell.command.completion", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
static class EnabledCondition {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -15,15 +15,10 @@
|
||||
*/
|
||||
package org.springframework.shell.boot;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.env.SystemEnvironmentPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -46,26 +41,25 @@ public class SpringShellPropertiesTests {
|
||||
assertThat(properties.getCommand().getQuit().isEnabled()).isTrue();
|
||||
assertThat(properties.getCommand().getScript().isEnabled()).isTrue();
|
||||
assertThat(properties.getCommand().getStacktrace().isEnabled()).isTrue();
|
||||
assertThat(properties.getCommand().getCompletion().isEnabled()).isTrue();
|
||||
assertThat(properties.getCommand().getCompletion().getRootCommand()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setProperties() {
|
||||
this.contextRunner
|
||||
.withInitializer(context -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("spring.shell.script.enabled", "false");
|
||||
map.put("spring.shell.interactive.enabled", "false");
|
||||
map.put("spring.shell.noninteractive.enabled", "false");
|
||||
map.put("spring.shell.command.clear.enabled", "false");
|
||||
map.put("spring.shell.command.help.enabled", "false");
|
||||
map.put("spring.shell.command.history.enabled", "false");
|
||||
map.put("spring.shell.command.quit.enabled", "false");
|
||||
map.put("spring.shell.command.script.enabled", "false");
|
||||
map.put("spring.shell.command.stacktrace.enabled", "false");
|
||||
context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource(
|
||||
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map));
|
||||
})
|
||||
.withPropertyValues("spring.shell.script.enabled=false")
|
||||
.withPropertyValues("spring.shell.interactive.enabled=false")
|
||||
.withPropertyValues("spring.shell.noninteractive.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.clear.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.help.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.history.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.quit.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.script.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.stacktrace.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.completion.enabled=false")
|
||||
.withPropertyValues("spring.shell.command.completion.root-command=fake")
|
||||
.withUserConfiguration(Config1.class)
|
||||
.run((context) -> {
|
||||
SpringShellProperties properties = context.getBean(SpringShellProperties.class);
|
||||
@@ -78,6 +72,8 @@ public class SpringShellPropertiesTests {
|
||||
assertThat(properties.getCommand().getQuit().isEnabled()).isFalse();
|
||||
assertThat(properties.getCommand().getScript().isEnabled()).isFalse();
|
||||
assertThat(properties.getCommand().getStacktrace().isEnabled()).isFalse();
|
||||
assertThat(properties.getCommand().getCompletion().isEnabled()).isFalse();
|
||||
assertThat(properties.getCommand().getCompletion().getRootCommand()).isEqualTo("fake");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 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.boot;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.shell.standard.commands.Completion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class StandardCommandsAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(StandardCommandsAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
public void testCompletionCommand() {
|
||||
this.contextRunner
|
||||
.with(disableCommands("help", "clear", "quit", "stacktrace", "script", "history"))
|
||||
.run((context) -> {assertThat(context).doesNotHaveBean(Completion.class);
|
||||
});
|
||||
this.contextRunner
|
||||
.with(disableCommands("help", "clear", "quit", "stacktrace", "script", "history", "completion"))
|
||||
.withPropertyValues("spring.shell.command.completion.root-command=fake")
|
||||
.run((context) -> {assertThat(context).doesNotHaveBean(Completion.class);
|
||||
});
|
||||
this.contextRunner
|
||||
.with(disableCommands("help", "clear", "quit", "stacktrace", "script", "history"))
|
||||
.withPropertyValues("spring.shell.command.completion.root-command=fake")
|
||||
.run((context) -> {assertThat(context).hasSingleBean(Completion.class);
|
||||
});
|
||||
}
|
||||
|
||||
private static Function<ApplicationContextRunner, ApplicationContextRunner> disableCommands(String... commands) {
|
||||
return (cr) -> {
|
||||
for (String command : commands) {
|
||||
cr = cr.withPropertyValues(String.format("spring.shell.command.%s.enabled=false", command));
|
||||
}
|
||||
return cr;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
logging:
|
||||
level:
|
||||
root: error
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
shell:
|
||||
command:
|
||||
completion:
|
||||
root-command: spring-shell-samples
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 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.commands;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.completion.BashCompletions;
|
||||
|
||||
/**
|
||||
* Command to create a shell completion files, i.e. for {@code bash}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@ShellComponent
|
||||
public class Completion extends AbstractShellComponent implements ResourceLoaderAware {
|
||||
|
||||
/**
|
||||
* Marker interface used in auto-config.
|
||||
*/
|
||||
public interface Command {
|
||||
}
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
private String rootCommand;
|
||||
|
||||
public Completion(String rootCommand) {
|
||||
this.rootCommand = rootCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "completion bash", value = "Generate bash completion script")
|
||||
public String bash() {
|
||||
BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandRegistry(),
|
||||
getParameterResolver().collect(Collectors.toList()));
|
||||
return bashCompletions.generate(rootCommand);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@
|
||||
<groupId>org.springframework.shell</groupId>
|
||||
<artifactId>spring-shell-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>ST4</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Copyright 2022 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 java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.stringtemplate.v4.ST;
|
||||
import org.stringtemplate.v4.STGroup;
|
||||
import org.stringtemplate.v4.STGroupString;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Base class for completion script commands providing functionality for
|
||||
* resource handling and templating with {@code antrl stringtemplate}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractCompletions {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final List<ParameterResolver> parameterResolvers;
|
||||
|
||||
public AbstractCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.parameterResolvers = parameterResolvers;
|
||||
}
|
||||
|
||||
protected Builder builder() {
|
||||
return new DefaultBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a model for a recursive command model starting from root
|
||||
* level going down with all sub commands with options. Essentially providing
|
||||
* all needed to build completions structure.
|
||||
*/
|
||||
protected CommandModel generateCommandModel() {
|
||||
Map<String, MethodTarget> commandsByName = commandRegistry.listCommands();
|
||||
HashMap<String, DefaultCommandModelCommand> commands = new HashMap<>();
|
||||
HashSet<CommandModelCommand> topCommands = new HashSet<>();
|
||||
commandsByName.entrySet().stream()
|
||||
.forEach(entry -> {
|
||||
String key = entry.getKey();
|
||||
String[] splitKeys = key.split(" ");
|
||||
String commandKey = "";
|
||||
for (int i = 0; i < splitKeys.length; i++) {
|
||||
DefaultCommandModelCommand parent = null;
|
||||
String main = splitKeys[i];
|
||||
if (i > 0) {
|
||||
parent = commands.get(commandKey);
|
||||
commandKey = commandKey + " " + splitKeys[i];
|
||||
}
|
||||
else {
|
||||
commandKey = splitKeys[i];
|
||||
}
|
||||
DefaultCommandModelCommand command = commands.computeIfAbsent(commandKey,
|
||||
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main));
|
||||
MethodTarget methodTarget = entry.getValue();
|
||||
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
||||
List<DefaultCommandModelOption> options = parameterDescriptions.stream()
|
||||
.flatMap(pd -> pd.keys().stream())
|
||||
.map(k -> new DefaultCommandModelOption(k))
|
||||
.collect(Collectors.toList());
|
||||
if (i == splitKeys.length - 1) {
|
||||
command.addOptions(options);
|
||||
}
|
||||
if (parent != null) {
|
||||
parent.addCommand(command);
|
||||
}
|
||||
if (i == 0) {
|
||||
topCommands.add(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
return new DefaultCommandModel(new ArrayList<>(topCommands));
|
||||
}
|
||||
|
||||
private List<ParameterDescription> getParameterDescriptions(MethodTarget methodTarget) {
|
||||
return Utils.createMethodParameters(methodTarget.getMethod())
|
||||
.flatMap(mp -> parameterResolvers.stream().filter(pr -> pr.supports(mp)).limit(1L)
|
||||
.flatMap(pr -> pr.describe(mp)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a command model structure. Is also used as entry model
|
||||
* for ST4 templates which is a reason it has utility methods for easier usage
|
||||
* of a templates.
|
||||
*/
|
||||
interface CommandModel {
|
||||
|
||||
/**
|
||||
* Gets root level commands where sub-commands can be found.
|
||||
*
|
||||
* @return root level commands
|
||||
*/
|
||||
List<CommandModelCommand> getCommands();
|
||||
|
||||
/**
|
||||
* Gets all commands as a flattened structure.
|
||||
*
|
||||
* @return all commands
|
||||
*/
|
||||
List<CommandModelCommand> getAllCommands();
|
||||
|
||||
/**
|
||||
* Gets root commands.
|
||||
*
|
||||
* @return root commands
|
||||
*/
|
||||
List<String> getRootCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a command in a model. Also contains methods which makes it
|
||||
* easier to work with ST4 templates.
|
||||
*/
|
||||
interface CommandModelCommand {
|
||||
|
||||
/**
|
||||
* Gets sub-commands known to this command.
|
||||
|
||||
* @return known sub-commands
|
||||
*/
|
||||
List<CommandModelCommand> getCommands();
|
||||
|
||||
/**
|
||||
* Gets options known to this command
|
||||
*
|
||||
* @return known options
|
||||
*/
|
||||
List<CommandModelOption> getOptions();
|
||||
|
||||
/**
|
||||
* Gets command flags.
|
||||
*
|
||||
* @return command flags
|
||||
*/
|
||||
List<String> getFlags();
|
||||
|
||||
/**
|
||||
* Gets sub commands.
|
||||
*
|
||||
* @return sub commands
|
||||
*/
|
||||
List<String> getSubCommands();
|
||||
|
||||
/**
|
||||
* Gets command parts. Essentially full command split into parts.
|
||||
*
|
||||
* @return command parts
|
||||
*/
|
||||
List<String> getCommandParts();
|
||||
|
||||
/**
|
||||
* Gets a main command
|
||||
*
|
||||
* @return the main command
|
||||
*/
|
||||
String getMainCommand();
|
||||
|
||||
/**
|
||||
* Gets a last command part.
|
||||
*
|
||||
* @return the last command part
|
||||
*/
|
||||
String getLastCommandPart();
|
||||
}
|
||||
|
||||
interface CommandModelOption {
|
||||
String option();
|
||||
}
|
||||
|
||||
class DefaultCommandModel implements CommandModel {
|
||||
|
||||
private final List<CommandModelCommand> commands;
|
||||
|
||||
public DefaultCommandModel(List<CommandModelCommand> commands) {
|
||||
this.commands = commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandModelCommand> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandModelCommand> getAllCommands() {
|
||||
return getCommands().stream()
|
||||
.flatMap(c -> flatten(c))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRootCommands() {
|
||||
return getCommands().stream()
|
||||
.map(c -> c.getLastCommandPart())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Stream<CommandModelCommand> flatten(CommandModelCommand command) {
|
||||
return Stream.concat(Stream.of(command), command.getCommands().stream().flatMap(c -> flatten(c)));
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModelCommand implements CommandModelCommand {
|
||||
|
||||
private String fullCommand;
|
||||
private String mainCommand;
|
||||
private List<CommandModelCommand> commands = new ArrayList<>();
|
||||
private List<CommandModelOption> options = new ArrayList<>();
|
||||
|
||||
DefaultCommandModelCommand(String fullCommand, String mainCommand) {
|
||||
this.fullCommand = fullCommand;
|
||||
this.mainCommand = mainCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getCommandParts() {
|
||||
return Arrays.asList(fullCommand.split(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLastCommandPart() {
|
||||
String[] split = fullCommand.split(" ");
|
||||
return split[split.length - 1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMainCommand() {
|
||||
return mainCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSubCommands() {
|
||||
return this.commands.stream()
|
||||
.map(c -> c.getMainCommand())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getFlags() {
|
||||
return this.options.stream()
|
||||
.map(o -> o.option())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandModelCommand> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandModelOption> getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
void addOptions(List<DefaultCommandModelOption> options) {
|
||||
this.options.addAll(options);
|
||||
}
|
||||
|
||||
void addCommand(DefaultCommandModelCommand command) {
|
||||
commands.add(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + getEnclosingInstance().hashCode();
|
||||
result = prime * result + ((fullCommand == null) ? 0 : fullCommand.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DefaultCommandModelCommand other = (DefaultCommandModelCommand) obj;
|
||||
if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
|
||||
return false;
|
||||
}
|
||||
if (fullCommand == null) {
|
||||
if (other.fullCommand != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!fullCommand.equals(other.fullCommand)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private AbstractCompletions getEnclosingInstance() {
|
||||
return AbstractCompletions.this;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModelOption implements CommandModelOption {
|
||||
|
||||
private String option;
|
||||
|
||||
public DefaultCommandModelOption(String option) {
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String option() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
private static String resourceAsString(Resource resource) {
|
||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return FileCopyUtils.copyToString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
interface Builder {
|
||||
|
||||
Builder attribute(String name, Object value);
|
||||
Builder group(String resource);
|
||||
Builder appendGroup(String instance);
|
||||
String build();
|
||||
}
|
||||
|
||||
class DefaultBuilder implements Builder {
|
||||
|
||||
private final MultiValueMap<String, Object> defaultAttributes = new LinkedMultiValueMap<>();
|
||||
private final List<Supplier<String>> operations = new ArrayList<>();
|
||||
private String groupResource;
|
||||
|
||||
@Override
|
||||
public Builder attribute(String name, Object value) {
|
||||
this.defaultAttributes.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder group(String resource) {
|
||||
groupResource = resource;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder appendGroup(String instance) {
|
||||
// delay so that we render with build
|
||||
Supplier<String> operation = () -> {
|
||||
String template = resourceAsString(resourceLoader.getResource(groupResource));
|
||||
STGroup group = new STGroupString(template);
|
||||
ST st = group.getInstanceOf(instance);
|
||||
defaultAttributes.entrySet().stream().forEach(entry -> {
|
||||
String key = entry.getKey();
|
||||
List<Object> values = entry.getValue();
|
||||
values.stream().forEach(v -> {
|
||||
st.add(key, v);
|
||||
});
|
||||
});
|
||||
return st.render();
|
||||
};
|
||||
operations.add(operation);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
operations.stream().forEach(operation -> {
|
||||
buf.append(operation.get());
|
||||
});
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2022 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 java.util.List;
|
||||
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
|
||||
/**
|
||||
* Completion script generator for a {@code bash}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class BashCompletions extends AbstractCompletions {
|
||||
|
||||
public BashCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
}
|
||||
|
||||
public String generate(String rootCommand) {
|
||||
CommandModel model = generateCommandModel();
|
||||
return builder()
|
||||
.attribute("name", rootCommand)
|
||||
.attribute("model", model)
|
||||
.group("classpath:completion/bash.stg")
|
||||
.appendGroup("main")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "org.springframework.shell.standard.completion.AbstractCompletions$DefaultCommandModel",
|
||||
"allDeclaredMethods": true
|
||||
},
|
||||
{
|
||||
"name": "org.springframework.shell.standard.completion.AbstractCompletions$DefaultCommandModelCommand",
|
||||
"allDeclaredMethods": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"resources": {
|
||||
"includes": [
|
||||
{
|
||||
"pattern": "completion/.*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
492
spring-shell-standard/src/main/resources/completion/bash.stg
Normal file
492
spring-shell-standard/src/main/resources/completion/bash.stg
Normal file
@@ -0,0 +1,492 @@
|
||||
//
|
||||
// pre content template before commands
|
||||
// needs to escape some > characters
|
||||
//
|
||||
pre(name) ::= <<
|
||||
# bash completion for <name> -*- shell-script -*-
|
||||
__<name>_debug()
|
||||
{
|
||||
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
|
||||
echo "$*" \>> "${BASH_COMP_DEBUG_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
|
||||
# _init_completion. This is a very minimal version of that function.
|
||||
__<name>_init_completion()
|
||||
{
|
||||
COMPREPLY=()
|
||||
_get_comp_words_by_ref "$@" cur prev words cword
|
||||
}
|
||||
__<name>_index_of_word()
|
||||
{
|
||||
local w word=$1
|
||||
shift
|
||||
index=0
|
||||
for w in "$@"; do
|
||||
[[ $w = "$word" ]] && return
|
||||
index=$((index+1))
|
||||
done
|
||||
index=-1
|
||||
}
|
||||
__<name>_contains_word()
|
||||
{
|
||||
local w word=$1; shift
|
||||
for w in "$@"; do
|
||||
[[ $w = "$word" ]] && return
|
||||
done
|
||||
return 1
|
||||
}
|
||||
__<name>_handle_go_custom_completion()
|
||||
{
|
||||
__<name>_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
|
||||
local shellCompDirectiveError=%[3]d
|
||||
local shellCompDirectiveNoSpace=%[4]d
|
||||
local shellCompDirectiveNoFileComp=%[5]d
|
||||
local shellCompDirectiveFilterFileExt=%[6]d
|
||||
local shellCompDirectiveFilterDirs=%[7]d
|
||||
local out requestComp lastParam lastChar comp directive args
|
||||
# Prepare the command to request completions for the program.
|
||||
# Calling ${words[0]} instead of directly <name> allows to handle aliases
|
||||
args=("${words[@]:1}")
|
||||
requestComp="${words[0]} %[2]s ${args[*]}"
|
||||
lastParam=${words[$((${#words[@]}-1))]}
|
||||
lastChar=${lastParam:$((${#lastParam}-1)):1}
|
||||
__<name>_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
|
||||
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
|
||||
# If the last parameter is complete (there is a space following it)
|
||||
# We add an extra empty parameter so we can indicate this to the go method.
|
||||
__<name>_debug "${FUNCNAME[0]}: Adding extra empty parameter"
|
||||
requestComp="${requestComp} \"\""
|
||||
fi
|
||||
__<name>_debug "${FUNCNAME[0]}: calling ${requestComp}"
|
||||
# Use eval to handle any environment variables and such
|
||||
out=$(eval "${requestComp}" 2>/dev/null)
|
||||
# Extract the directive integer at the very end of the output following a colon (:)
|
||||
directive=${out##*:}
|
||||
# Remove the directive
|
||||
out=${out%%:*}
|
||||
if [ "${directive}" = "${out}" ]; then
|
||||
# There is not directive specified
|
||||
directive=0
|
||||
fi
|
||||
__<name>_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
|
||||
__<name>_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
|
||||
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
|
||||
# Error code. No completion.
|
||||
__<name>_debug "${FUNCNAME[0]}: received error from custom completion go code"
|
||||
return
|
||||
else
|
||||
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
__<name>_debug "${FUNCNAME[0]}: activating no space"
|
||||
compopt -o nospace
|
||||
fi
|
||||
fi
|
||||
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
__<name>_debug "${FUNCNAME[0]}: activating no file completion"
|
||||
compopt +o default
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
|
||||
# File extension filtering
|
||||
local fullFilter filter filteringCmd
|
||||
# Do not use quotes around the $out variable or else newline
|
||||
# characters will be kept.
|
||||
for filter in ${out[*]}; do
|
||||
fullFilter+="$filter|"
|
||||
done
|
||||
filteringCmd="_filedir $fullFilter"
|
||||
__<name>_debug "File filtering command: $filteringCmd"
|
||||
$filteringCmd
|
||||
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
|
||||
# File completion for directories only
|
||||
local subdir
|
||||
# Use printf to strip any trailing newline
|
||||
subdir=$(printf "%%s" "${out[0]}")
|
||||
if [ -n "$subdir" ]; then
|
||||
__<name>_debug "Listing directories in $subdir"
|
||||
__<name>_handle_subdirs_in_dir_flag "$subdir"
|
||||
else
|
||||
__<name>_debug "Listing directories in ."
|
||||
_filedir -d
|
||||
fi
|
||||
else
|
||||
while IFS='' read -r comp; do
|
||||
COMPREPLY+=("$comp")
|
||||
done \< \<(compgen -W "${out[*]}" -- "$cur")
|
||||
fi
|
||||
}
|
||||
__<name>_handle_reply()
|
||||
{
|
||||
__<name>_debug "${FUNCNAME[0]}"
|
||||
local comp
|
||||
case $cur in
|
||||
-*)
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
compopt -o nospace
|
||||
fi
|
||||
local allflags
|
||||
if [ ${#must_have_one_flag[@]} -ne 0 ]; then
|
||||
allflags=("${must_have_one_flag[@]}")
|
||||
else
|
||||
allflags=("${flags[*]} ${two_word_flags[*]}")
|
||||
fi
|
||||
while IFS='' read -r comp; do
|
||||
COMPREPLY+=("$comp")
|
||||
done \< \<(compgen -W "${allflags[*]}" -- "$cur")
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
[[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
|
||||
fi
|
||||
# complete after --flag=abc
|
||||
if [[ $cur == *=* ]]; then
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
compopt +o nospace
|
||||
fi
|
||||
local index flag
|
||||
flag="${cur%%=*}"
|
||||
__<name>_index_of_word "${flag}" "${flags_with_completion[@]}"
|
||||
COMPREPLY=()
|
||||
if [[ ${index} -ge 0 ]]; then
|
||||
PREFIX=""
|
||||
cur="${cur#*=}"
|
||||
${flags_completion[${index}]}
|
||||
if [ -n "${ZSH_VERSION:-}" ]; then
|
||||
# zsh completion needs --flag= prefix
|
||||
eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${flag_parsing_disabled}" ]]; then
|
||||
# If flag parsing is enabled, we have completed the flags and can return.
|
||||
# If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
|
||||
# to possibly call handle_go_custom_completion.
|
||||
return 0;
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# check if we are handling a flag with special work handling
|
||||
local index
|
||||
__<name>_index_of_word "${prev}" "${flags_with_completion[@]}"
|
||||
if [[ ${index} -ge 0 ]]; then
|
||||
${flags_completion[${index}]}
|
||||
return
|
||||
fi
|
||||
# we are parsing a flag and don't have a special handler, no completion
|
||||
if [[ ${cur} != "${words[cword]}" ]]; then
|
||||
return
|
||||
fi
|
||||
local completions
|
||||
completions=("${commands[@]}")
|
||||
if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
|
||||
completions+=("${must_have_one_noun[@]}")
|
||||
elif [[ -n "${has_completion_function}" ]]; then
|
||||
# if a go completion function is provided, defer to that function
|
||||
__<name>_handle_go_custom_completion
|
||||
fi
|
||||
if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
|
||||
completions+=("${must_have_one_flag[@]}")
|
||||
fi
|
||||
while IFS='' read -r comp; do
|
||||
COMPREPLY+=("$comp")
|
||||
done \< \<(compgen -W "${completions[*]}" -- "$cur")
|
||||
if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
|
||||
while IFS='' read -r comp; do
|
||||
COMPREPLY+=("$comp")
|
||||
done \< \<(compgen -W "${noun_aliases[*]}" -- "$cur")
|
||||
fi
|
||||
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
|
||||
if declare -F __<name>_custom_func >/dev/null; then
|
||||
# try command name qualified custom func
|
||||
__<name>_custom_func
|
||||
else
|
||||
# otherwise fall back to unqualified for compatibility
|
||||
declare -F __custom_func >/dev/null && __custom_func
|
||||
fi
|
||||
fi
|
||||
# available in bash-completion >= 2, not always present on macOS
|
||||
if declare -F __ltrim_colon_completions >/dev/null; then
|
||||
__ltrim_colon_completions "$cur"
|
||||
fi
|
||||
# If there is only 1 completion and it is a flag with an = it will be completed
|
||||
# but we don't want a space after the =
|
||||
if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
|
||||
compopt -o nospace
|
||||
fi
|
||||
}
|
||||
# The arguments should be in the form "ext1|ext2|extn"
|
||||
__<name>_handle_filename_extension_flag()
|
||||
{
|
||||
local ext="$1"
|
||||
_filedir "@(${ext})"
|
||||
}
|
||||
__<name>_handle_subdirs_in_dir_flag()
|
||||
{
|
||||
local dir="$1"
|
||||
pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
|
||||
}
|
||||
__<name>_handle_flag()
|
||||
{
|
||||
__<name>_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
|
||||
# if a command required a flag, and we found it, unset must_have_one_flag()
|
||||
local flagname=${words[c]}
|
||||
local flagvalue=""
|
||||
# if the word contained an =
|
||||
if [[ ${words[c]} == *"="* ]]; then
|
||||
flagvalue=${flagname#*=} # take in as flagvalue after the =
|
||||
flagname=${flagname%%=*} # strip everything after the =
|
||||
flagname="${flagname}=" # but put the = back
|
||||
fi
|
||||
__<name>_debug "${FUNCNAME[0]}: looking for ${flagname}"
|
||||
if __<name>_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
|
||||
must_have_one_flag=()
|
||||
fi
|
||||
# if you set a flag which only applies to this command, don't show subcommands
|
||||
if __<name>_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
|
||||
commands=()
|
||||
fi
|
||||
# keep flag value with flagname as flaghash
|
||||
# flaghash variable is an associative array which is only supported in bash > 3.
|
||||
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
|
||||
if [ -n "${flagvalue}" ] ; then
|
||||
flaghash[${flagname}]=${flagvalue}
|
||||
elif [ -n "${words[ $((c+1)) ]}" ] ; then
|
||||
flaghash[${flagname}]=${words[ $((c+1)) ]}
|
||||
else
|
||||
flaghash[${flagname}]="true" # pad "true" for bool flag
|
||||
fi
|
||||
fi
|
||||
# skip the argument to a two word flag
|
||||
if [[ ${words[c]} != *"="* ]] && __<name>_contains_word "${words[c]}" "${two_word_flags[@]}"; then
|
||||
__<name>_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
|
||||
c=$((c+1))
|
||||
# if we are looking for a flags value, don't show commands
|
||||
if [[ $c -eq $cword ]]; then
|
||||
commands=()
|
||||
fi
|
||||
fi
|
||||
c=$((c+1))
|
||||
}
|
||||
__<name>_handle_noun()
|
||||
{
|
||||
__<name>_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
|
||||
if __<name>_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
|
||||
must_have_one_noun=()
|
||||
elif __<name>_contains_word "${words[c]}" "${noun_aliases[@]}"; then
|
||||
must_have_one_noun=()
|
||||
fi
|
||||
nouns+=("${words[c]}")
|
||||
c=$((c+1))
|
||||
}
|
||||
__<name>_handle_command()
|
||||
{
|
||||
__<name>_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
|
||||
local next_command
|
||||
if [[ -n ${last_command} ]]; then
|
||||
next_command="_${last_command}_${words[c]//:/__}"
|
||||
else
|
||||
if [[ $c -eq 0 ]]; then
|
||||
next_command="_<name>_root_command"
|
||||
else
|
||||
next_command="_${words[c]//:/__}"
|
||||
fi
|
||||
fi
|
||||
c=$((c+1))
|
||||
__<name>_debug "${FUNCNAME[0]}: looking for ${next_command}"
|
||||
declare -F "$next_command" >/dev/null && $next_command
|
||||
}
|
||||
__<name>_handle_word()
|
||||
{
|
||||
if [[ $c -ge $cword ]]; then
|
||||
__<name>_handle_reply
|
||||
return
|
||||
fi
|
||||
__<name>_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
|
||||
if [[ "${words[c]}" == -* ]]; then
|
||||
__<name>_handle_flag
|
||||
elif __<name>_contains_word "${words[c]}" "${commands[@]}"; then
|
||||
__<name>_handle_command
|
||||
elif [[ $c -eq 0 ]]; then
|
||||
__<name>_handle_command
|
||||
elif __<name>_contains_word "${words[c]}" "${command_aliases[@]}"; then
|
||||
# aliashash variable is an associative array which is only supported in bash > 3.
|
||||
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
|
||||
words[c]=${aliashash[${words[c]}]}
|
||||
__<name>_handle_command
|
||||
else
|
||||
__<name>_handle_noun
|
||||
fi
|
||||
else
|
||||
__<name>_handle_noun
|
||||
fi
|
||||
__<name>_handle_word
|
||||
}
|
||||
>>
|
||||
|
||||
//
|
||||
// post content template after commands
|
||||
//
|
||||
post(name) ::= <<
|
||||
__start_<name>()
|
||||
{
|
||||
local cur prev words cword split
|
||||
declare -A flaghash 2>/dev/null || :
|
||||
declare -A aliashash 2>/dev/null || :
|
||||
if declare -F _init_completion >/dev/null 2>&1; then
|
||||
_init_completion -s || return
|
||||
else
|
||||
__<name>_init_completion -n "=" || return
|
||||
fi
|
||||
local c=0
|
||||
local flag_parsing_disabled=
|
||||
local flags=()
|
||||
local two_word_flags=()
|
||||
local local_nonpersistent_flags=()
|
||||
local flags_with_completion=()
|
||||
local flags_completion=()
|
||||
local commands=("<name>")
|
||||
local command_aliases=()
|
||||
local must_have_one_flag=()
|
||||
local must_have_one_noun=()
|
||||
local has_completion_function=""
|
||||
local last_command=""
|
||||
local nouns=()
|
||||
local noun_aliases=()
|
||||
__<name>_handle_word
|
||||
}
|
||||
|
||||
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||
complete -o default -F __start_<name> <name>
|
||||
else
|
||||
complete -o default -o nospace -F __start_<name> <name>
|
||||
fi
|
||||
>>
|
||||
|
||||
//
|
||||
// command_aliases=() template section in commands
|
||||
//
|
||||
command_aliases() ::= <<
|
||||
command_aliases=()
|
||||
>>
|
||||
|
||||
//
|
||||
// commands=() template section in commands
|
||||
//
|
||||
commands(commands) ::= <<
|
||||
commands=()
|
||||
<commands:{c | commands+=("<c>")}; separator="\n">
|
||||
>>
|
||||
|
||||
//
|
||||
// flags=() template section in commands
|
||||
//
|
||||
flags() ::= <<
|
||||
flags=()
|
||||
>>
|
||||
|
||||
//
|
||||
// two_word_flags=() template section in commands
|
||||
//
|
||||
two_word_flags(flags) ::= <<
|
||||
two_word_flags=()
|
||||
<flags:{f | two_word_flags+=("<f>")}; separator="\n">
|
||||
>>
|
||||
|
||||
//
|
||||
// local_nonpersistent_flags=() template section in commands
|
||||
//
|
||||
local_nonpersistent_flags() ::= <<
|
||||
local_nonpersistent_flags=()
|
||||
>>
|
||||
|
||||
//
|
||||
// flags_with_completion=() template section in commands
|
||||
//
|
||||
flags_with_completion() ::= <<
|
||||
flags_with_completion=()
|
||||
>>
|
||||
|
||||
//
|
||||
// flags_completion=() template section in commands
|
||||
//
|
||||
flags_completion() ::= <<
|
||||
flags_completion=()
|
||||
>>
|
||||
|
||||
//
|
||||
// must_have_one_flag=() template section in commands
|
||||
//
|
||||
must_have_one_flag() ::= <<
|
||||
must_have_one_flag=()
|
||||
>>
|
||||
|
||||
//
|
||||
// must_have_one_noun=() template section in commands
|
||||
//
|
||||
must_have_one_noun() ::= <<
|
||||
must_have_one_noun=()
|
||||
>>
|
||||
|
||||
//
|
||||
// noun_aliases=() template section in commands
|
||||
//
|
||||
noun_aliases() ::= <<
|
||||
noun_aliases=()
|
||||
>>
|
||||
|
||||
//
|
||||
// template for each command
|
||||
//
|
||||
sub_command(name,command) ::= <<
|
||||
_<name>_<command.commandParts:{p | <p>}; separator="_">()
|
||||
{
|
||||
last_command="<name>_<c.commandParts:{p | <p>}; separator="_">"
|
||||
|
||||
<command_aliases()>
|
||||
<commands(command.subCommands)>
|
||||
<flags()>
|
||||
<two_word_flags(command.flags)>
|
||||
<local_nonpersistent_flags()>
|
||||
<flags_with_completion()>
|
||||
<flags_completion()>
|
||||
<must_have_one_flag()>
|
||||
<must_have_one_noun()>
|
||||
<noun_aliases()>
|
||||
}
|
||||
>>
|
||||
|
||||
//
|
||||
// top level root commands template
|
||||
//
|
||||
root_commands(name,commands) ::= <<
|
||||
_<name>_root_command()
|
||||
{
|
||||
last_command="<name>"
|
||||
|
||||
<command_aliases()>
|
||||
<commands(commands)>
|
||||
<flags()>
|
||||
<two_word_flags([])>
|
||||
<local_nonpersistent_flags()>
|
||||
<flags_with_completion()>
|
||||
<flags_completion()>
|
||||
<must_have_one_flag()>
|
||||
<must_have_one_noun()>
|
||||
<noun_aliases()>
|
||||
}
|
||||
>>
|
||||
|
||||
//
|
||||
// main template to call from render
|
||||
//
|
||||
main(name, model) ::= <<
|
||||
<pre(name)>
|
||||
|
||||
<model.allCommands:{c | <sub_command(name,c)>}; separator="\n\n">
|
||||
|
||||
<root_commands(name,model.rootCommands)>
|
||||
|
||||
<post(name)>
|
||||
>>
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2022 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 java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.shell.standard.StandardParameterResolver;
|
||||
import org.springframework.shell.standard.completion.AbstractCompletions.CommandModel;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class AbstractCompletionsTests {
|
||||
|
||||
@Test
|
||||
public void testBasicModelGeneration() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry();
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
parameterResolvers.add(resolver);
|
||||
|
||||
TestCommands commands = new TestCommands();
|
||||
|
||||
Method method1 = ReflectionUtils.findMethod(TestCommands.class, "test1", String.class);
|
||||
Method method2 = ReflectionUtils.findMethod(TestCommands.class, "test2");
|
||||
Method method3 = ReflectionUtils.findMethod(TestCommands.class, "test3");
|
||||
Method method4 = ReflectionUtils.findMethod(TestCommands.class, "test4", String.class);
|
||||
|
||||
MethodTarget methodTarget1 = new MethodTarget(method1, commands, "help");
|
||||
MethodTarget methodTarget2 = new MethodTarget(method2, commands, "help");
|
||||
MethodTarget methodTarget3 = new MethodTarget(method3, commands, "help");
|
||||
MethodTarget methodTarget4 = new MethodTarget(method4, commands, "help");
|
||||
|
||||
commandRegistry.register("test1", methodTarget1);
|
||||
commandRegistry.register("test2", methodTarget2);
|
||||
commandRegistry.register("test3", methodTarget3);
|
||||
commandRegistry.register("test3 test4", methodTarget4);
|
||||
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
CommandModel commandModel = completions.testCommandModel();
|
||||
assertThat(commandModel.getCommands()).hasSize(3);
|
||||
assertThat(commandModel.getCommands().stream().map(c -> c.getMainCommand())).containsExactlyInAnyOrder("test1", "test2",
|
||||
"test3");
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test1")).findFirst().get()
|
||||
.getOptions()).hasSize(1);
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test1")).findFirst().get()
|
||||
.getOptions().get(0).option()).isEqualTo("--param1");
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test2")).findFirst().get()
|
||||
.getOptions()).hasSize(0);
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test3")).findFirst().get()
|
||||
.getOptions()).hasSize(0);
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test3")).findFirst().get()
|
||||
.getCommands()).hasSize(1);
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test3")).findFirst().get()
|
||||
.getCommands().get(0).getMainCommand()).isEqualTo("test4");
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test3")).findFirst().get()
|
||||
.getCommands().get(0).getOptions()).hasSize(1);
|
||||
assertThat(commandModel.getCommands().stream().filter(c -> c.getMainCommand().equals("test3")).findFirst().get()
|
||||
.getCommands().get(0).getOptions().get(0).option()).isEqualTo("--param4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry();
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
|
||||
String result = completions.testBuilder()
|
||||
.attribute("x", "command")
|
||||
.group("classpath:completion/test.stg")
|
||||
.appendGroup("a")
|
||||
.build();
|
||||
assertThat(result).contains("foocommand");
|
||||
}
|
||||
|
||||
private static class TestCompletions extends AbstractCompletions {
|
||||
|
||||
public TestCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
}
|
||||
|
||||
CommandModel testCommandModel() {
|
||||
return generateCommandModel();
|
||||
}
|
||||
|
||||
Builder testBuilder() {
|
||||
return super.builder();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestCommands {
|
||||
|
||||
@ShellMethod
|
||||
void test1(@ShellOption String param1) {
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
void test2() {
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
void test3() {
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
void test4(@ShellOption String param4) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2022 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class BashCompletionsTests {
|
||||
|
||||
AnnotationConfigApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
context = new AnnotationConfigApplicationContext();
|
||||
context.refresh();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void clean() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
context = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotError() {
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry();
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
BashCompletions completions = new BashCompletions(context, commandRegistry, parameterResolvers);
|
||||
String bash = completions.generate("root-command");
|
||||
assertThat(bash).contains("root-command");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
b(y) ::= "<y>"
|
||||
a(x) ::= <<
|
||||
foo<b(x)>
|
||||
>>
|
||||
Reference in New Issue
Block a user