diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java new file mode 100644 index 00000000..9575b0a7 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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 + * + * http://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.jline; + +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.springframework.shell.Input; +import org.springframework.shell.InputProvider; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; + +/** + * An {@link InputProvider} that reads input from file-like sources. + *

+ *

Still uses a {@link org.jline.reader.Parser} to interpret word boundaries. Supports backslashes at end + * of line to signal line continuation.

+ * + * @author Eric Bottard + */ +public class FileInputProvider implements InputProvider, Closeable { + + private static final String BACKSLASH_AT_EOL_REGEX = "(.*)\\\\\\s*$"; + private final BufferedReader reader; + + private final Parser parser; + + public FileInputProvider(Reader reader, Parser parser) { + this.reader = new BufferedReader(reader); + this.parser = parser; + } + + @Override + public Input readInput() { + StringBuilder sb = new StringBuilder(); + boolean continued = false; + String line; + try { + do { + line = reader.readLine(); + if (line == null) { + break; + } + continued = line.matches(BACKSLASH_AT_EOL_REGEX); + sb.append(line.replaceFirst(BACKSLASH_AT_EOL_REGEX, "$1 ")); + } while (continued); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (line == null) { + return null; + } else { + ParsedLine parsedLine = parser.parse(sb.toString(), sb.toString().length()); + return new ParsedLineInput(parsedLine); + } + } + + @Override + public void close() throws IOException { + reader.close(); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java index b7cc5110..3d496d96 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java @@ -22,13 +22,7 @@ import java.util.stream.Collectors; import javax.annotation.PostConstruct; -import org.jline.reader.Candidate; -import org.jline.reader.Completer; -import org.jline.reader.Highlighter; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.ParsedLine; -import org.jline.reader.UserInterruptException; +import org.jline.reader.*; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.utils.AttributedString; @@ -101,11 +95,15 @@ class JLineShellAutoConfiguration { } @Bean - public LineReader lineReader() { + public Parser parser() { ExtendedDefaultParser parser = new ExtendedDefaultParser(); parser.setEofOnUnclosedQuote(true); parser.setEofOnEscapedNewLine(true); + return parser; + } + @Bean + public LineReader lineReader() { LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder() .terminal(terminal()) .appName("Spring Shell") @@ -130,7 +128,7 @@ class JLineShellAutoConfiguration { } } }) - .parser(parser); + .parser(parser()); return lineReaderBuilder.build(); } @@ -139,7 +137,7 @@ class JLineShellAutoConfiguration { * Sanitize the buffer input given the customizations applied to the JLine parser (e.g. support for * line continuations, etc.) */ - static private List sanitizeInput(List words) { + static List sanitizeInput(List words) { words = words.stream() .map(s -> s.replaceAll("^\\n+|\\n+$", "")) // CR at beginning/end of line introduced by backslash continuation .map(s -> s.replaceAll("\\n+", " ")) // CR in middle of word introduced by return inside a quoted string @@ -204,30 +202,11 @@ class JLineShellAutoConfiguration { return Input.EMPTY; } } - return new JLineInput(lineReader.getParsedLine()); + return new ParsedLineInput(lineReader.getParsedLine()); } } - private static class JLineInput implements Input { - - private final ParsedLine parsedLine; - - JLineInput(ParsedLine parsedLine) { - this.parsedLine = parsedLine; - } - - @Override - public String rawText() { - return parsedLine.line(); - } - - @Override - public List words() { - return sanitizeInput(parsedLine.words()); - } - } - } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/ParsedLineInput.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/ParsedLineInput.java new file mode 100644 index 00000000..b9c623f3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/ParsedLineInput.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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 + * + * http://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.jline; + +import org.jline.reader.ParsedLine; +import org.springframework.shell.Input; + +import java.util.List; + +/** + * An implementation of {@link Input} backed by the result of a {@link org.jline.reader.Parser#parse(String, int)}. + * + * @author Eric Bottard + */ +class ParsedLineInput implements Input { + + private final ParsedLine parsedLine; + + ParsedLineInput(ParsedLine parsedLine) { + this.parsedLine = parsedLine; + } + + @Override + public String rawText() { + return parsedLine.line(); + } + + @Override + public List words() { + return JLineShellAutoConfiguration.sanitizeInput(parsedLine.words()); + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java new file mode 100644 index 00000000..ebfa64e7 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java @@ -0,0 +1,58 @@ +package org.springframework.shell.standard.commands; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +import org.jline.reader.Parser; +import org.springframework.shell.Input; +import org.springframework.shell.Shell; +import org.springframework.shell.jline.FileInputProvider; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; + +/** + * A command that can read and execute other commands from a file. + * + * @author Eric Bottard + */ +@ShellComponent +public class Script { + + private final Shell shell; + + private final Parser parser; + + public Script(Shell shell, Parser parser) { + this.shell = shell; + this.parser = parser; + } + + /** + * Marker interface for beans providing {@literal script} functionality to the shell. + *

+ *

To override the script command, simply register your own bean implementing that interface + * and the standard implementation will back off.

+ *

+ *

To disable the {@literal script} command entirely, set the {@literal spring.shell.command.script.enabled=false} + * property in the environment.

+ * + * @author Eric Bottard + */ + public interface Command { + } + + @ShellMethod(value = "Read and execute commands from a file") + public void script(File file) throws IOException { + Reader reader = new FileReader(file); + try (FileInputProvider inputProvider = new FileInputProvider(reader, parser)) { + Input input = inputProvider.readInput(); + while (input != null) { + shell.evaluate(input); + input = inputProvider.readInput(); + } + } + } + +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java index a40d9785..c6c8218a 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.shell.standard.commands; import java.util.List; +import org.jline.reader.Parser; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -61,4 +62,10 @@ public class StandardCommandsAutoConfiguration { return new Stacktrace(); } + @Bean + @ConditionalOnMissingBean(Script.Command.class) + @ConditionalOnProperty(prefix = "spring.shell.command.script", value = "enabled", havingValue = "true", matchIfMissing = true) + public Script script(Shell shell, Parser parser) { + return new Script(shell, parser); + } } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/CommandValueProvider.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/CommandValueProvider.java index c4d5ba05..6338b146 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/CommandValueProvider.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/CommandValueProvider.java @@ -32,7 +32,6 @@ import org.springframework.stereotype.Component; * * @author Eric Bottard */ -@Component public class CommandValueProvider extends ValueProviderSupport { private final CommandRegistry commandRegistry; diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/EnumValueProvider.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/EnumValueProvider.java index cfaa6703..baa1645f 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/EnumValueProvider.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/EnumValueProvider.java @@ -28,7 +28,6 @@ import org.springframework.stereotype.Component; * A {@link ValueProvider} that knows how to complete values for {@link Enum} typed parameters. * @author Eric Bottard */ -@Component public class EnumValueProvider implements ValueProvider { @Override diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java new file mode 100644 index 00000000..7485e4a7 --- /dev/null +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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 + * + * http://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; + +import org.springframework.core.MethodParameter; +import org.springframework.shell.CompletionContext; +import org.springframework.shell.CompletionProposal; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import static java.nio.file.FileVisitOption.FOLLOW_LINKS; + +/** + * A {@link ValueProvider} that can populate names of local {@link File}s, either absolute or relative to the + * current working directory. + * + * @author Eric Bottard + */ +public class FileValueProvider implements ValueProvider { + @Override + public boolean supports(MethodParameter parameter, CompletionContext completionContext) { + return parameter.getParameterType().equals(File.class); + } + + @Override + public List complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) { + + String input = completionContext.currentWordUpToCursor(); + int lastSlash = input.lastIndexOf(File.separatorChar); + Path dir = lastSlash > -1 ? Paths.get(input.substring(0, lastSlash+1)) : Paths.get(""); + String prefix = input.substring(lastSlash + 1, input.length()); + + try { + return Files.find(dir, 1, (p, a) -> p.getFileName() != null && p.getFileName().toString().startsWith(prefix), FOLLOW_LINKS) + .map(p -> new CompletionProposal(p.toString())) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + } + +} diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardAPIAutoConfiguration.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardAPIAutoConfiguration.java index 1e839f3a..9f949f0d 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardAPIAutoConfiguration.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardAPIAutoConfiguration.java @@ -41,6 +41,11 @@ public class StandardAPIAutoConfiguration { return new EnumValueProvider(); } + @Bean + public ValueProvider fileValueProvider() { + return new FileValueProvider(); + } + @Bean public MethodTargetRegistrar standardMethodTargetResolver() { return new StandardMethodTargetRegistrar(); diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/ValueProvider.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/ValueProvider.java index bd5f07de..2b19fa58 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/ValueProvider.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/ValueProvider.java @@ -23,6 +23,9 @@ import org.springframework.shell.CompletionContext; import org.springframework.shell.CompletionProposal; /** + * Beans implementing this interface are queried during TAB completion to gather possible values of a parameter. + * + * @author Eric Bottard */ public interface ValueProvider {