Skip to content

Commit 1ce48ff

Browse files
committed
Add support to read (secure) input from commands
1 parent 70a37c4 commit 1ce48ff

File tree

10 files changed

+224
-14
lines changed

10 files changed

+224
-14
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2025-present 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.core;
17+
18+
import java.io.Console;
19+
20+
/**
21+
* Implementation of {@link InputReader} that reads input from the system console.
22+
*
23+
* @author Mahmoud Ben Hassine
24+
* @since 4.0.0
25+
*/
26+
public class ConsoleInputReader implements InputReader {
27+
28+
private final Console console;
29+
30+
/**
31+
* Create a new {@link ConsoleInputReader} instance.
32+
* @param console the system console
33+
*/
34+
public ConsoleInputReader(Console console) {
35+
this.console = console;
36+
}
37+
38+
@Override
39+
public String readInput() {
40+
return this.console.readLine();
41+
}
42+
43+
@Override
44+
public String readInput(String prompt) {
45+
return this.console.readLine(prompt);
46+
}
47+
48+
@Override
49+
public char[] readPassword() {
50+
return this.console.readPassword();
51+
}
52+
53+
@Override
54+
public char[] readPassword(String prompt) {
55+
return this.console.readPassword(prompt);
56+
}
57+
58+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025-present 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.core;
17+
18+
/**
19+
* Interface for reading input from the user.
20+
*
21+
* @author Mahmoud Ben Hassine
22+
* @since 4.0.0
23+
*/
24+
public interface InputReader {
25+
26+
/**
27+
* Read a line of input from the user.
28+
* @return the input line
29+
* @throws Exception if an error occurs while reading input
30+
*/
31+
default String readInput() throws Exception {
32+
return readInput("");
33+
}
34+
35+
/**
36+
* Read a line of input from the user with a prompt.
37+
* @param prompt the prompt to display to the user
38+
* @return the input line
39+
* @throws Exception if an error occurs while reading input
40+
*/
41+
default String readInput(String prompt) throws Exception {
42+
throw new UnsupportedOperationException("readInput with prompt not implemented");
43+
}
44+
45+
/**
46+
* Read a password from the user.
47+
* @return the password as a character array
48+
* @throws Exception if an error occurs while reading the password
49+
*/
50+
default char[] readPassword() throws Exception {
51+
return readPassword("");
52+
}
53+
54+
/**
55+
* Read a password from the user with a prompt.
56+
* @param prompt the prompt to display to the user
57+
* @return the password as a character array
58+
* @throws Exception if an error occurs while reading the password
59+
*/
60+
default char[] readPassword(String prompt) throws Exception {
61+
throw new UnsupportedOperationException("readPassword with prompt not implemented");
62+
}
63+
64+
}

spring-shell-core/src/main/java/org/springframework/shell/core/InteractiveShellRunner.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ public void run(String[] args) throws Exception {
9494
}
9595
ParsedInput parsedInput = this.commandParser.parse(input);
9696
try {
97-
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, getWriter());
97+
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, getWriter(),
98+
getReader());
9899
ExitStatus exitStatus = this.commandExecutor.execute(commandContext);
99100
if (ExitStatus.OK.code() != exitStatus.code()) { // business error
100101
print("Error while executing command " + parsedInput.commandName() + ": "
@@ -136,4 +137,10 @@ public void run(String[] args) throws Exception {
136137
*/
137138
public abstract PrintWriter getWriter();
138139

140+
/**
141+
* Get the input reader.
142+
* @return the input reader
143+
*/
144+
public abstract InputReader getReader();
145+
139146
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public class NonInteractiveShellRunner implements ShellRunner {
5151
// Use a no-op PrintWriter since output is not needed in non-interactive mode
5252
private final PrintWriter outputWriter = new PrintWriter(PrintWriter.nullWriter());
5353

54+
// Use a no-op InputReader since input is not needed in non-interactive mode
55+
private final InputReader inputReader = new InputReader() {
56+
};
57+
5458
/**
5559
* Create a new {@link NonInteractiveShellRunner} instance.
5660
* @param commandParser the command parser
@@ -105,7 +109,8 @@ private void executeScript(File script) {
105109
break;
106110
}
107111
ParsedInput parsedInput = this.commandParser.parse(input);
108-
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, this.outputWriter);
112+
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, this.outputWriter,
113+
this.inputReader);
109114
ExitStatus exitStatus = this.commandExecutor.execute(commandContext);
110115
if (ExitStatus.OK.code() != exitStatus.code()) { // business error
111116
log.error("Command " + parsedInput.commandName() + " returned an error: " + exitStatus.description()
@@ -117,7 +122,8 @@ private void executeScript(File script) {
117122

118123
private void executeCommand(String primaryCommand) {
119124
ParsedInput parsedInput = this.commandParser.parse(primaryCommand);
120-
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, this.outputWriter);
125+
CommandContext commandContext = new CommandContext(parsedInput, this.commandRegistry, this.outputWriter,
126+
this.inputReader);
121127
ExitStatus exitStatus = this.commandExecutor.execute(commandContext);
122128
if (ExitStatus.OK.code() != exitStatus.code()) {
123129
log.error("Command " + parsedInput.commandName() + " returned an error: " + exitStatus.description());

spring-shell-core/src/main/java/org/springframework/shell/core/SystemShellRunner.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,9 @@ public PrintWriter getWriter() {
6060
return this.console.writer();
6161
}
6262

63+
@Override
64+
public InputReader getReader() {
65+
return new ConsoleInputReader(this.console);
66+
}
67+
6368
}

spring-shell-core/src/main/java/org/springframework/shell/core/command/CommandContext.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919

2020
import org.jspecify.annotations.Nullable;
2121

22+
import org.springframework.shell.core.InputReader;
23+
2224
/**
2325
* Interface containing runtime information about the current command invocation.
2426
*
2527
* @author Mahmoud Ben Hassine
2628
* @since 4.0.0
2729
*/
28-
public record CommandContext(ParsedInput parsedInput, CommandRegistry commandRegistry, PrintWriter outputWriter) {
30+
public record CommandContext(ParsedInput parsedInput, CommandRegistry commandRegistry, PrintWriter outputWriter,
31+
InputReader inputReader) {
2932

3033
/**
3134
* Retrieve a command option by its name (long or short).

spring-shell-core/src/test/java/org/springframework/shell/core/command/CommandAvailabilityTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.junit.jupiter.api.Test;
2222

23+
import org.springframework.shell.core.InputReader;
2324
import org.springframework.shell.core.command.availability.Availability;
2425
import org.springframework.shell.core.command.availability.AvailabilityProvider;
2526

@@ -50,7 +51,7 @@ public ExitStatus doExecute(CommandContext commandContext) {
5051
};
5152
StringWriter stringWriter = new StringWriter();
5253
CommandContext commandContext = new CommandContext(mock(ParsedInput.class), mock(CommandRegistry.class),
53-
new PrintWriter(stringWriter));
54+
new PrintWriter(stringWriter), mock(InputReader.class));
5455

5556
// when
5657
ExitStatus exitStatus = command.execute(commandContext);

spring-shell-jline/src/main/java/org/springframework/shell/jline/JLineInputProvider.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.springframework.shell.jline;
22

33
import org.jline.reader.LineReader;
4-
import org.jline.terminal.Terminal;
54
import org.jline.utils.AttributedString;
65
import org.jline.utils.AttributedStyle;
76

@@ -33,8 +32,8 @@ public void setPromptProvider(PromptProvider promptProvider) {
3332
this.promptProvider = promptProvider;
3433
}
3534

36-
public Terminal getTerminal() {
37-
return this.lineReader.getTerminal();
35+
public LineReader getLineReader() {
36+
return this.lineReader;
3837
}
3938

4039
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2025-present 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.jline;
17+
18+
import org.jline.reader.LineReader;
19+
20+
import org.springframework.shell.core.InputReader;
21+
22+
/**
23+
* Implementation of {@link InputReader} that reads input using JLine's
24+
* {@link LineReader}.
25+
*
26+
* @author Mahmoud Ben Hassine
27+
* @since 4.0.0
28+
*/
29+
public class JLineInputReader implements InputReader {
30+
31+
private final LineReader lineReader;
32+
33+
/**
34+
* Create a new {@link JLineInputReader} instance.
35+
* @param lineReader the JLine line reader
36+
*/
37+
public JLineInputReader(LineReader lineReader) {
38+
this.lineReader = lineReader;
39+
}
40+
41+
@Override
42+
public String readInput() {
43+
return lineReader.readLine();
44+
}
45+
46+
@Override
47+
public String readInput(String prompt) {
48+
return lineReader.readLine(prompt);
49+
}
50+
51+
@Override
52+
public char[] readPassword() {
53+
return lineReader.readLine('*').toCharArray();
54+
}
55+
56+
@Override
57+
public char[] readPassword(String prompt) {
58+
return lineReader.readLine(prompt, '*').toCharArray();
59+
}
60+
61+
}

spring-shell-jline/src/main/java/org/springframework/shell/jline/JLineShellRunner.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
import java.io.Console;
1919
import java.io.PrintWriter;
2020

21-
import org.jline.terminal.Terminal;
21+
import org.jline.reader.LineReader;
2222

23+
import org.springframework.shell.core.InputReader;
2324
import org.springframework.shell.core.InteractiveShellRunner;
2425
import org.springframework.shell.core.command.CommandParser;
2526
import org.springframework.shell.core.command.CommandRegistry;
@@ -32,7 +33,7 @@
3233
*/
3334
public class JLineShellRunner extends InteractiveShellRunner {
3435

35-
private final Terminal terminal;
36+
private final LineReader lineReader;
3637

3738
/**
3839
* Create a new {@link JLineShellRunner} instance.
@@ -43,22 +44,27 @@ public class JLineShellRunner extends InteractiveShellRunner {
4344
public JLineShellRunner(JLineInputProvider inputProvider, CommandParser commandParser,
4445
CommandRegistry commandRegistry) {
4546
super(inputProvider, commandParser, commandRegistry);
46-
this.terminal = inputProvider.getTerminal();
47+
this.lineReader = inputProvider.getLineReader();
4748
}
4849

4950
@Override
5051
public void print(String message) {
51-
this.terminal.writer().println(message);
52+
this.lineReader.getTerminal().writer().println(message);
5253
}
5354

5455
@Override
5556
public void flush() {
56-
this.terminal.flush();
57+
lineReader.getTerminal().flush();
5758
}
5859

5960
@Override
6061
public PrintWriter getWriter() {
61-
return this.terminal.writer();
62+
return this.lineReader.getTerminal().writer();
63+
}
64+
65+
@Override
66+
public InputReader getReader() {
67+
return new JLineInputReader(this.lineReader);
6268
}
6369

6470
}

0 commit comments

Comments
 (0)