Add primary command feature

- NonInteractiveShellRunner can now shortcircuit into primary command
  just running it and passing args.
- Add hooks into autoconfig so that this is easy to configure.
- Relates #755
This commit is contained in:
Janne Valkealahti
2023-07-05 10:33:55 +01:00
parent 4ee1907f00
commit 32675c5e73
7 changed files with 169 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors.
* Copyright 2021-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.
@@ -21,8 +21,13 @@ import org.jline.reader.Parser;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.Shell;
import org.springframework.shell.boot.condition.OnNotPrimaryCommandCondition;
import org.springframework.shell.boot.condition.OnPrimaryCommandCondition;
import org.springframework.shell.context.ShellContext;
import org.springframework.shell.jline.InteractiveShellRunner;
import org.springframework.shell.jline.NonInteractiveShellRunner;
@@ -32,38 +37,48 @@ import org.springframework.shell.jline.ScriptShellRunner;
@AutoConfiguration
public class ShellRunnerAutoConfiguration {
private Shell shell;
private PromptProvider promptProvider;
private LineReader lineReader;
private Parser parser;
private ShellContext shellContext;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SpringShellProperties.class)
@Conditional(OnPrimaryCommandCondition.class)
public static class PrimaryCommandConfiguration {
@Bean
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
public NonInteractiveShellRunner nonInteractiveApplicationRunner(Shell shell, ShellContext shellContext,
ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer, SpringShellProperties properties) {
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext,
properties.getNoninteractive().getPrimaryCommand());
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
return shellRunner;
}
public ShellRunnerAutoConfiguration(Shell shell, PromptProvider promptProvider, LineReader lineReader,
Parser parser, ShellContext shellContext) {
this.shell = shell;
this.promptProvider = promptProvider;
this.lineReader = lineReader;
this.parser = parser;
this.shellContext = shellContext;
}
@Bean
@ConditionalOnProperty(prefix = "spring.shell.interactive", value = "enabled", havingValue = "true", matchIfMissing = true)
public InteractiveShellRunner interactiveApplicationRunner() {
return new InteractiveShellRunner(lineReader, promptProvider, shell, shellContext);
}
@Configuration(proxyBeanMethods = false)
@Conditional(OnNotPrimaryCommandCondition.class)
public static class NonePrimaryCommandConfiguration {
@Bean
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
public NonInteractiveShellRunner nonInteractiveApplicationRunner(ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer) {
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
return shellRunner;
}
@Bean
@ConditionalOnProperty(prefix = "spring.shell.interactive", value = "enabled", havingValue = "true", matchIfMissing = true)
public InteractiveShellRunner interactiveApplicationRunner(LineReader lineReader, PromptProvider promptProvider,
Shell shell, ShellContext shellContext) {
return new InteractiveShellRunner(lineReader, promptProvider, shell, shellContext);
}
@Bean
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
public NonInteractiveShellRunner nonInteractiveApplicationRunner(Shell shell, ShellContext shellContext,
ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer) {
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
return shellRunner;
}
@Bean
@ConditionalOnProperty(prefix = "spring.shell.script", value = "enabled", havingValue = "true", matchIfMissing = true)
public ScriptShellRunner scriptApplicationRunner(Parser parser, Shell shell) {
return new ScriptShellRunner(parser, shell);
}
@Bean
@ConditionalOnProperty(prefix = "spring.shell.script", value = "enabled", havingValue = "true", matchIfMissing = true)
public ScriptShellRunner scriptApplicationRunner() {
return new ScriptShellRunner(parser, shell);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-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.
@@ -180,6 +180,7 @@ public class SpringShellProperties {
public static class Noninteractive {
private boolean enabled = true;
private String primaryCommand;
public boolean isEnabled() {
return enabled;
@@ -188,6 +189,14 @@ public class SpringShellProperties {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getPrimaryCommand() {
return primaryCommand;
}
public void setPrimaryCommand(String primaryCommand) {
this.primaryCommand = primaryCommand;
}
}
public static class Theme {

View File

@@ -0,0 +1,31 @@
/*
* 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.boot.condition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
public class OnNotPrimaryCommandCondition extends NoneNestedConditions {
public OnNotPrimaryCommandCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "primary-command", matchIfMissing = false)
static class NotPrimaryCommandCondition {
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.boot.condition;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
public class OnPrimaryCommandCondition extends AllNestedConditions {
public OnPrimaryCommandCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "primary-command", matchIfMissing = false)
static class PrimaryCommandCondition {
}
}

View File

@@ -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.
@@ -30,6 +30,7 @@ import org.springframework.shell.jline.InteractiveShellRunner;
import org.springframework.shell.jline.NonInteractiveShellRunner;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.shell.jline.ScriptShellRunner;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -71,6 +72,16 @@ class ShellRunnerAutoConfigurationTests {
contextRunner.run(context -> assertThat(context).hasSingleBean(NonInteractiveShellRunner.class));
}
@Test
void primaryCommandNotSet() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(NonInteractiveShellRunner.class);
NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class);
String command = (String) ReflectionTestUtils.getField(runner, "primaryCommand");
assertThat(command).isNull();
});
}
@Test
void disabledWhenPropertySet() {
contextRunner.withPropertyValues("spring.shell.noninteractive.enabled:false")
@@ -101,4 +112,22 @@ class ShellRunnerAutoConfigurationTests {
.run(context -> assertThat(context).doesNotHaveBean(ScriptShellRunner.class));
}
}
@Nested
class PrimaryCommand {
@Test
void primaryCommandDisablesOtherRunners() {
contextRunner.withPropertyValues("spring.shell.noninteractive.primary-command:fake")
.run(context -> {
assertThat(context).doesNotHaveBean(InteractiveShellRunner.class);
assertThat(context).doesNotHaveBean(ScriptShellRunner.class);
assertThat(context).hasSingleBean(NonInteractiveShellRunner.class);
NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class);
String command = (String) ReflectionTestUtils.getField(runner, "primaryCommand");
assertThat(command).isEqualTo("fake");
});
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-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.
@@ -41,6 +41,7 @@ public class SpringShellPropertiesTests {
assertThat(properties.getScript().isEnabled()).isTrue();
assertThat(properties.getInteractive().isEnabled()).isTrue();
assertThat(properties.getNoninteractive().isEnabled()).isTrue();
assertThat(properties.getNoninteractive().getPrimaryCommand()).isNull();
assertThat(properties.getTheme().getName()).isNull();
assertThat(properties.getCommand().getClear().isEnabled()).isTrue();
assertThat(properties.getCommand().getHelp().isEnabled()).isTrue();
@@ -82,6 +83,7 @@ public class SpringShellPropertiesTests {
.withPropertyValues("spring.shell.script.enabled=false")
.withPropertyValues("spring.shell.interactive.enabled=false")
.withPropertyValues("spring.shell.noninteractive.enabled=false")
.withPropertyValues("spring.shell.noninteractive.primary-command=fakecommand")
.withPropertyValues("spring.shell.theme.name=fake")
.withPropertyValues("spring.shell.command.clear.enabled=false")
.withPropertyValues("spring.shell.command.help.enabled=false")
@@ -120,6 +122,7 @@ public class SpringShellPropertiesTests {
assertThat(properties.getScript().isEnabled()).isFalse();
assertThat(properties.getInteractive().isEnabled()).isFalse();
assertThat(properties.getNoninteractive().isEnabled()).isFalse();
assertThat(properties.getNoninteractive().getPrimaryCommand()).isEqualTo("fakecommand");
assertThat(properties.getTheme().getName()).isEqualTo("fake");
assertThat(properties.getCommand().getClear().isEnabled()).isFalse();
assertThat(properties.getCommand().getHelp().isEnabled()).isFalse();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-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.
@@ -60,12 +60,19 @@ public class NonInteractiveShellRunner implements ShellRunner {
private Parser lineParser;
private String primaryCommand;
private static final String SINGLE_QUOTE = "\'";
private static final String DOUBLE_QUOTE = "\"";
private Function<ApplicationArguments, List<String>> commandsFromInputArgs = args -> {
if (args.getSourceArgs().length == 0) {
return Collections.emptyList();
if (StringUtils.hasText(primaryCommand)) {
Collections.singletonList(primaryCommand);
}
else {
return Collections.emptyList();
}
}
// re-quote if needed having whitespace
String raw = Arrays.stream(args.getSourceArgs())
@@ -76,7 +83,12 @@ public class NonInteractiveShellRunner implements ShellRunner {
return a;
})
.collect(Collectors.joining(" "));
return Collections.singletonList(raw);
if (StringUtils.hasText(primaryCommand)) {
return Collections.singletonList(primaryCommand + " " + raw);
}
else {
return Collections.singletonList(raw);
}
};
private static boolean isQuoted(String str) {
@@ -88,8 +100,13 @@ public class NonInteractiveShellRunner implements ShellRunner {
}
public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) {
this(shell, shellContext, null);
}
public NonInteractiveShellRunner(Shell shell, ShellContext shellContext, String primaryCommand) {
this.shell = shell;
this.shellContext = shellContext;
this.primaryCommand = primaryCommand;
this.lineParser = new DefaultParser();
}