Skip to content

Commit 051f7da

Browse files
committed
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. - Backport #755 - Relates #799
1 parent f388a30 commit 051f7da

File tree

7 files changed

+169
-35
lines changed

7 files changed

+169
-35
lines changed
Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 the original author or authors.
2+
* Copyright 2021-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.
@@ -21,8 +21,13 @@
2121
import org.springframework.beans.factory.ObjectProvider;
2222
import org.springframework.boot.autoconfigure.AutoConfiguration;
2323
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
24+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2425
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Conditional;
27+
import org.springframework.context.annotation.Configuration;
2528
import org.springframework.shell.Shell;
29+
import org.springframework.shell.boot.condition.OnNotPrimaryCommandCondition;
30+
import org.springframework.shell.boot.condition.OnPrimaryCommandCondition;
2631
import org.springframework.shell.context.ShellContext;
2732
import org.springframework.shell.jline.InteractiveShellRunner;
2833
import org.springframework.shell.jline.NonInteractiveShellRunner;
@@ -32,38 +37,48 @@
3237
@AutoConfiguration
3338
public class ShellRunnerAutoConfiguration {
3439

35-
private Shell shell;
36-
private PromptProvider promptProvider;
37-
private LineReader lineReader;
38-
private Parser parser;
39-
private ShellContext shellContext;
40+
@Configuration(proxyBeanMethods = false)
41+
@EnableConfigurationProperties(SpringShellProperties.class)
42+
@Conditional(OnPrimaryCommandCondition.class)
43+
public static class PrimaryCommandConfiguration {
4044

41-
public ShellRunnerAutoConfiguration(Shell shell, PromptProvider promptProvider, LineReader lineReader,
42-
Parser parser, ShellContext shellContext) {
43-
this.shell = shell;
44-
this.promptProvider = promptProvider;
45-
this.lineReader = lineReader;
46-
this.parser = parser;
47-
this.shellContext = shellContext;
48-
}
45+
@Bean
46+
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
47+
public NonInteractiveShellRunner nonInteractiveApplicationRunner(Shell shell, ShellContext shellContext,
48+
ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer, SpringShellProperties properties) {
49+
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext,
50+
properties.getNoninteractive().getPrimaryCommand());
51+
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
52+
return shellRunner;
53+
}
4954

50-
@Bean
51-
@ConditionalOnProperty(prefix = "spring.shell.interactive", value = "enabled", havingValue = "true", matchIfMissing = true)
52-
public InteractiveShellRunner interactiveApplicationRunner() {
53-
return new InteractiveShellRunner(lineReader, promptProvider, shell, shellContext);
5455
}
5556

56-
@Bean
57-
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
58-
public NonInteractiveShellRunner nonInteractiveApplicationRunner(ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer) {
59-
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
60-
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
61-
return shellRunner;
62-
}
57+
@Configuration(proxyBeanMethods = false)
58+
@Conditional(OnNotPrimaryCommandCondition.class)
59+
public static class NonePrimaryCommandConfiguration {
60+
61+
@Bean
62+
@ConditionalOnProperty(prefix = "spring.shell.interactive", value = "enabled", havingValue = "true", matchIfMissing = true)
63+
public InteractiveShellRunner interactiveApplicationRunner(LineReader lineReader, PromptProvider promptProvider,
64+
Shell shell, ShellContext shellContext) {
65+
return new InteractiveShellRunner(lineReader, promptProvider, shell, shellContext);
66+
}
67+
68+
@Bean
69+
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
70+
public NonInteractiveShellRunner nonInteractiveApplicationRunner(Shell shell, ShellContext shellContext,
71+
ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer) {
72+
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
73+
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
74+
return shellRunner;
75+
}
76+
77+
@Bean
78+
@ConditionalOnProperty(prefix = "spring.shell.script", value = "enabled", havingValue = "true", matchIfMissing = true)
79+
public ScriptShellRunner scriptApplicationRunner(Parser parser, Shell shell) {
80+
return new ScriptShellRunner(parser, shell);
81+
}
6382

64-
@Bean
65-
@ConditionalOnProperty(prefix = "spring.shell.script", value = "enabled", havingValue = "true", matchIfMissing = true)
66-
public ScriptShellRunner scriptApplicationRunner() {
67-
return new ScriptShellRunner(parser, shell);
6883
}
6984
}

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2022 the original author or authors.
2+
* Copyright 2021-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.
@@ -180,6 +180,7 @@ public void setEnabled(boolean enabled) {
180180
public static class Noninteractive {
181181

182182
private boolean enabled = true;
183+
private String primaryCommand;
183184

184185
public boolean isEnabled() {
185186
return enabled;
@@ -188,6 +189,14 @@ public boolean isEnabled() {
188189
public void setEnabled(boolean enabled) {
189190
this.enabled = enabled;
190191
}
192+
193+
public String getPrimaryCommand() {
194+
return primaryCommand;
195+
}
196+
197+
public void setPrimaryCommand(String primaryCommand) {
198+
this.primaryCommand = primaryCommand;
199+
}
191200
}
192201

193202
public static class Theme {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.boot.condition;
17+
18+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
19+
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
20+
21+
public class OnNotPrimaryCommandCondition extends NoneNestedConditions {
22+
23+
public OnNotPrimaryCommandCondition() {
24+
super(ConfigurationPhase.REGISTER_BEAN);
25+
}
26+
27+
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "primary-command", matchIfMissing = false)
28+
static class NotPrimaryCommandCondition {
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.boot.condition;
17+
18+
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
19+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20+
21+
public class OnPrimaryCommandCondition extends AllNestedConditions {
22+
23+
public OnPrimaryCommandCondition() {
24+
super(ConfigurationPhase.REGISTER_BEAN);
25+
}
26+
27+
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "primary-command", matchIfMissing = false)
28+
static class PrimaryCommandCondition {
29+
}
30+
}

spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ShellRunnerAutoConfigurationTests.java

Lines changed: 30 additions & 1 deletion
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.
@@ -30,6 +30,7 @@
3030
import org.springframework.shell.jline.NonInteractiveShellRunner;
3131
import org.springframework.shell.jline.PromptProvider;
3232
import org.springframework.shell.jline.ScriptShellRunner;
33+
import org.springframework.test.util.ReflectionTestUtils;
3334

3435
import static org.assertj.core.api.Assertions.assertThat;
3536
import static org.mockito.Mockito.mock;
@@ -71,6 +72,16 @@ void enabledByDefault() {
7172
contextRunner.run(context -> assertThat(context).hasSingleBean(NonInteractiveShellRunner.class));
7273
}
7374

75+
@Test
76+
void primaryCommandNotSet() {
77+
contextRunner.run(context -> {
78+
assertThat(context).hasSingleBean(NonInteractiveShellRunner.class);
79+
NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class);
80+
String command = (String) ReflectionTestUtils.getField(runner, "primaryCommand");
81+
assertThat(command).isNull();
82+
});
83+
}
84+
7485
@Test
7586
void disabledWhenPropertySet() {
7687
contextRunner.withPropertyValues("spring.shell.noninteractive.enabled:false")
@@ -101,4 +112,22 @@ void disabledWhenPropertySet() {
101112
.run(context -> assertThat(context).doesNotHaveBean(ScriptShellRunner.class));
102113
}
103114
}
115+
116+
@Nested
117+
class PrimaryCommand {
118+
119+
@Test
120+
void primaryCommandDisablesOtherRunners() {
121+
contextRunner.withPropertyValues("spring.shell.noninteractive.primary-command:fake")
122+
.run(context -> {
123+
assertThat(context).doesNotHaveBean(InteractiveShellRunner.class);
124+
assertThat(context).doesNotHaveBean(ScriptShellRunner.class);
125+
assertThat(context).hasSingleBean(NonInteractiveShellRunner.class);
126+
NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class);
127+
String command = (String) ReflectionTestUtils.getField(runner, "primaryCommand");
128+
assertThat(command).isEqualTo("fake");
129+
});
130+
}
131+
132+
}
104133
}

spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2022 the original author or authors.
2+
* Copyright 2021-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.
@@ -41,6 +41,7 @@ public void defaultNoPropertiesSet() {
4141
assertThat(properties.getScript().isEnabled()).isTrue();
4242
assertThat(properties.getInteractive().isEnabled()).isTrue();
4343
assertThat(properties.getNoninteractive().isEnabled()).isTrue();
44+
assertThat(properties.getNoninteractive().getPrimaryCommand()).isNull();
4445
assertThat(properties.getTheme().getName()).isNull();
4546
assertThat(properties.getCommand().getClear().isEnabled()).isTrue();
4647
assertThat(properties.getCommand().getHelp().isEnabled()).isTrue();
@@ -82,6 +83,7 @@ public void setProperties() {
8283
.withPropertyValues("spring.shell.script.enabled=false")
8384
.withPropertyValues("spring.shell.interactive.enabled=false")
8485
.withPropertyValues("spring.shell.noninteractive.enabled=false")
86+
.withPropertyValues("spring.shell.noninteractive.primary-command=fakecommand")
8587
.withPropertyValues("spring.shell.theme.name=fake")
8688
.withPropertyValues("spring.shell.command.clear.enabled=false")
8789
.withPropertyValues("spring.shell.command.help.enabled=false")
@@ -120,6 +122,7 @@ public void setProperties() {
120122
assertThat(properties.getScript().isEnabled()).isFalse();
121123
assertThat(properties.getInteractive().isEnabled()).isFalse();
122124
assertThat(properties.getNoninteractive().isEnabled()).isFalse();
125+
assertThat(properties.getNoninteractive().getPrimaryCommand()).isEqualTo("fakecommand");
123126
assertThat(properties.getTheme().getName()).isEqualTo("fake");
124127
assertThat(properties.getCommand().getClear().isEnabled()).isFalse();
125128
assertThat(properties.getCommand().getHelp().isEnabled()).isFalse();

spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2022 the original author or authors.
2+
* Copyright 2021-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.
@@ -60,12 +60,19 @@ public class NonInteractiveShellRunner implements ShellRunner {
6060

6161
private Parser lineParser;
6262

63+
private String primaryCommand;
64+
6365
private static final String SINGLE_QUOTE = "\'";
6466
private static final String DOUBLE_QUOTE = "\"";
6567

6668
private Function<ApplicationArguments, List<String>> commandsFromInputArgs = args -> {
6769
if (args.getSourceArgs().length == 0) {
68-
return Collections.emptyList();
70+
if (StringUtils.hasText(primaryCommand)) {
71+
Collections.singletonList(primaryCommand);
72+
}
73+
else {
74+
return Collections.emptyList();
75+
}
6976
}
7077
// re-quote if needed having whitespace
7178
String raw = Arrays.stream(args.getSourceArgs())
@@ -76,7 +83,12 @@ public class NonInteractiveShellRunner implements ShellRunner {
7683
return a;
7784
})
7885
.collect(Collectors.joining(" "));
79-
return Collections.singletonList(raw);
86+
if (StringUtils.hasText(primaryCommand)) {
87+
return Collections.singletonList(primaryCommand + " " + raw);
88+
}
89+
else {
90+
return Collections.singletonList(raw);
91+
}
8092
};
8193

8294
private static boolean isQuoted(String str) {
@@ -88,8 +100,13 @@ private static boolean isQuoted(String str) {
88100
}
89101

90102
public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) {
103+
this(shell, shellContext, null);
104+
}
105+
106+
public NonInteractiveShellRunner(Shell shell, ShellContext shellContext, String primaryCommand) {
91107
this.shell = shell;
92108
this.shellContext = shellContext;
109+
this.primaryCommand = primaryCommand;
93110
this.lineParser = new DefaultParser();
94111
}
95112

0 commit comments

Comments
 (0)