This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new e60743369b [MNG-8587] mvnsh navigation (#2117)
e60743369b is described below
commit e60743369b986cac180e61b67549adae2ef9ae09
Author: Tamas Cservenak <[email protected]>
AuthorDate: Mon Feb 24 13:58:03 2025 +0100
[MNG-8587] mvnsh navigation (#2117)
Ability to navigate on disk and new commands:
* `!` execute shell command (executable or script)
* `cd` change cwd
* `pwd` print cwd
* some annoying issues like ctrl+c and others are fixed as well
---
https://issues.apache.org/jira/browse/MNG-8587
---
.../java/org/apache/maven/cling/invoker/CWD.java | 78 ++++++++++
.../apache/maven/cling/invoker/LookupContext.java | 15 +-
.../apache/maven/cling/invoker/LookupInvoker.java | 37 ++---
.../invoker/PlexusContainerCapsuleFactory.java | 2 +-
.../maven/cling/invoker/mvn/MavenInvoker.java | 14 +-
.../maven/cling/invoker/mvnenc/EncryptInvoker.java | 4 +-
.../maven/cling/invoker/mvnsh/ShellInvoker.java | 47 ++++--
.../BuiltinShellCommandRegistryFactory.java | 160 +++++++++++++++------
8 files changed, 269 insertions(+), 88 deletions(-)
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CWD.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CWD.java
new file mode 100644
index 0000000000..c4492dbb75
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CWD.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.maven.cling.invoker;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+
+import org.apache.maven.api.annotations.Nonnull;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A thin wrapper for a {@link Path} that serves as "current working
directory" value. Hence, this class
+ * is mutable (as CWD may be changed), but allows transition only to existing
directories.
+ */
+public final class CWD implements Supplier<Path> {
+ /**
+ * Creates instance out of {@link Path}.
+ */
+ public static CWD create(Path path) {
+ return new CWD(Utils.getCanonicalPath(path));
+ }
+
+ private Path directory;
+
+ private CWD(Path directory) {
+ this.directory = directory;
+ }
+
+ @Nonnull
+ @Override
+ public Path get() {
+ return directory;
+ }
+
+ /**
+ * Resolves against current cwd, resulting path is normalized.
+ *
+ * @throws NullPointerException if {@code seg} is {@code null}.
+ */
+ @Nonnull
+ public Path resolve(String seg) {
+ requireNonNull(seg, "seg");
+ return directory.resolve(seg).normalize();
+ }
+
+ /**
+ * Changes current cwd, if the new path is existing directory.
+ *
+ * @throws NullPointerException if {@code seg} is {@code null}.
+ * @throws IllegalArgumentException if {@code seg} leads to non-existent
directory.
+ */
+ public void change(String seg) {
+ Path newCwd = resolve(seg);
+ if (Files.isDirectory(newCwd)) {
+ this.directory = newCwd;
+ } else {
+ throw new IllegalArgumentException("Directory '" + directory + "'
does not exist");
+ }
+ }
+}
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
index d468c54041..06bf49f1f8 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
@@ -26,7 +26,6 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
-import java.util.function.Function;
import org.apache.maven.api.ProtoSession;
import org.apache.maven.api.cli.InvokerException;
@@ -46,9 +45,9 @@
@SuppressWarnings("VisibilityModifier")
public class LookupContext implements AutoCloseable {
public final InvokerRequest invokerRequest;
- public final Function<String, Path> cwdResolver;
- public final Function<String, Path> installationResolver;
- public final Function<String, Path> userResolver;
+ public final CWD cwd;
+ public final Path installationDirectory;
+ public final Path userDirectory;
public final boolean containerCapsuleManaged;
public LookupContext(InvokerRequest invokerRequest) {
@@ -57,11 +56,9 @@ public LookupContext(InvokerRequest invokerRequest) {
public LookupContext(InvokerRequest invokerRequest, boolean
containerCapsuleManaged) {
this.invokerRequest = requireNonNull(invokerRequest);
- this.cwdResolver = s ->
invokerRequest.cwd().resolve(s).normalize().toAbsolutePath();
- this.installationResolver = s ->
-
invokerRequest.installationDirectory().resolve(s).normalize().toAbsolutePath();
- this.userResolver =
- s ->
invokerRequest.userHomeDirectory().resolve(s).normalize().toAbsolutePath();
+ this.cwd = CWD.create(invokerRequest.cwd());
+ this.installationDirectory =
Utils.getCanonicalPath(invokerRequest.installationDirectory());
+ this.userDirectory =
Utils.getCanonicalPath(invokerRequest.userHomeDirectory());
this.containerCapsuleManaged = containerCapsuleManaged;
this.logger = invokerRequest.parserRequest().logger();
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
index 0fb3d974e3..5ab134f19d 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
@@ -200,7 +200,9 @@ protected void printErrors(C context, boolean
showStackTrace, List<Logger.Entry>
protected void validate(C context) throws Exception {
if (context.invokerRequest.parsingFailed()) {
// in case of parser errors: report errors and bail out;
invokerRequest contents may be incomplete
- List<Logger.Entry> entries = context.logger.drain();
+ // in case of mvnsh the context.logger !=
context.invokerRequest.parserRequest.logger
+ List<Logger.Entry> entries =
+ context.invokerRequest.parserRequest().logger().drain();
printErrors(
context,
context.invokerRequest
@@ -367,7 +369,7 @@ protected Consumer<String> determineWriter(C context) {
protected Consumer<String> doDetermineWriter(C context) {
Options options = context.invokerRequest.options();
if (options.logFile().isPresent()) {
- Path logFile = context.cwdResolver.apply(options.logFile().get());
+ Path logFile = context.cwd.resolve(options.logFile().get());
try {
PrintWriter printWriter = new
PrintWriter(Files.newBufferedWriter(logFile), true);
context.closeables.add(printWriter);
@@ -507,10 +509,9 @@ protected void lookup(C context) throws Exception {
}
protected void init(C context) throws Exception {
- InvokerRequest invokerRequest = context.invokerRequest;
Map<String, Object> data = new HashMap<>();
data.put("plexus", context.lookup.lookup(PlexusContainer.class));
- data.put("workingDirectory", invokerRequest.cwd().toString());
+ data.put("workingDirectory", context.cwd.get().toString());
data.put("systemProperties",
toProperties(context.protoSession.getSystemProperties()));
data.put("userProperties",
toProperties(context.protoSession.getUserProperties()));
data.put("versionProperties", CLIReportingUtils.getBuildProperties());
@@ -567,7 +568,7 @@ protected Runnable settings(C context, boolean
emitSettingsWarnings, SettingsBui
Path userSettingsFile = null;
if (mavenOptions.altUserSettings().isPresent()) {
userSettingsFile =
-
context.cwdResolver.apply(mavenOptions.altUserSettings().get());
+ context.cwd.resolve(mavenOptions.altUserSettings().get());
if (!Files.isRegularFile(userSettingsFile)) {
throw new FileNotFoundException("The specified user settings
file does not exist: " + userSettingsFile);
@@ -576,14 +577,15 @@ protected Runnable settings(C context, boolean
emitSettingsWarnings, SettingsBui
String userSettingsFileStr =
context.protoSession.getUserProperties().get(Constants.MAVEN_USER_SETTINGS);
if (userSettingsFileStr != null) {
- userSettingsFile =
context.userResolver.apply(userSettingsFileStr);
+ userSettingsFile =
+
context.userDirectory.resolve(userSettingsFileStr).normalize();
}
}
Path projectSettingsFile = null;
if (mavenOptions.altProjectSettings().isPresent()) {
projectSettingsFile =
-
context.cwdResolver.apply(mavenOptions.altProjectSettings().get());
+
context.cwd.resolve(mavenOptions.altProjectSettings().get());
if (!Files.isRegularFile(projectSettingsFile)) {
throw new FileNotFoundException(
@@ -593,14 +595,14 @@ protected Runnable settings(C context, boolean
emitSettingsWarnings, SettingsBui
String projectSettingsFileStr =
context.protoSession.getUserProperties().get(Constants.MAVEN_PROJECT_SETTINGS);
if (projectSettingsFileStr != null) {
- projectSettingsFile =
context.cwdResolver.apply(projectSettingsFileStr);
+ projectSettingsFile =
context.cwd.resolve(projectSettingsFileStr);
}
}
Path installationSettingsFile = null;
if (mavenOptions.altInstallationSettings().isPresent()) {
- installationSettingsFile = context.cwdResolver.apply(
- mavenOptions.altInstallationSettings().get());
+ installationSettingsFile =
+
context.cwd.resolve(mavenOptions.altInstallationSettings().get());
if (!Files.isRegularFile(installationSettingsFile)) {
throw new FileNotFoundException(
@@ -610,7 +612,9 @@ protected Runnable settings(C context, boolean
emitSettingsWarnings, SettingsBui
String installationSettingsFileStr =
context.protoSession.getUserProperties().get(Constants.MAVEN_INSTALLATION_SETTINGS);
if (installationSettingsFileStr != null) {
- installationSettingsFile =
context.installationResolver.apply(installationSettingsFileStr);
+ installationSettingsFile = context.installationDirectory
+ .resolve(installationSettingsFileStr)
+ .normalize();
}
}
@@ -716,17 +720,18 @@ protected Path localRepositoryPath(C context) {
}
}
if (userDefinedLocalRepo != null) {
- return context.cwdResolver.apply(userDefinedLocalRepo);
+ return context.cwd.resolve(userDefinedLocalRepo);
}
// settings
userDefinedLocalRepo = context.effectiveSettings.getLocalRepository();
if (userDefinedLocalRepo != null && !userDefinedLocalRepo.isEmpty()) {
- return context.userResolver.apply(userDefinedLocalRepo);
+ return
context.userDirectory.resolve(userDefinedLocalRepo).normalize();
}
// defaults
- return context.userResolver
-
.apply(context.protoSession.getUserProperties().get(Constants.MAVEN_USER_CONF))
- .resolve("repository");
+ return context.userDirectory
+
.resolve(context.protoSession.getUserProperties().get(Constants.MAVEN_USER_CONF))
+ .resolve("repository")
+ .normalize();
}
protected void populateRequest(C context, Lookup lookup,
MavenExecutionRequest request) throws Exception {
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
index b793274db3..98c8618872 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
@@ -194,7 +194,7 @@ protected List<Path> parseExtClasspath(C context) throws
Exception {
ArrayList<Path> jars = new ArrayList<>();
if (extClassPath != null && !extClassPath.isEmpty()) {
for (String jar : extClassPath.split(File.pathSeparator)) {
- Path file = context.cwdResolver.apply(jar);
+ Path file = context.cwd.resolve(jar);
context.logger.debug(" included '" + file + "'");
jars.add(file);
}
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
index 393c31b782..325de096e7 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
@@ -142,7 +142,7 @@ protected void postCommands(MavenContext context) throws
Exception {
protected void toolchains(MavenContext context, MavenExecutionRequest
request) throws Exception {
Path userToolchainsFile = null;
if (context.invokerRequest.options().altUserToolchains().isPresent()) {
- userToolchainsFile = context.cwdResolver.apply(
+ userToolchainsFile = context.cwd.resolve(
context.invokerRequest.options().altUserToolchains().get());
if (!Files.isRegularFile(userToolchainsFile)) {
@@ -153,13 +153,13 @@ protected void toolchains(MavenContext context,
MavenExecutionRequest request) t
String userToolchainsFileStr =
context.protoSession.getUserProperties().get(Constants.MAVEN_USER_TOOLCHAINS);
if (userToolchainsFileStr != null) {
- userToolchainsFile =
context.cwdResolver.apply(userToolchainsFileStr);
+ userToolchainsFile =
context.cwd.resolve(userToolchainsFileStr);
}
}
Path installationToolchainsFile = null;
if
(context.invokerRequest.options().altInstallationToolchains().isPresent()) {
- installationToolchainsFile = context.cwdResolver.apply(
+ installationToolchainsFile = context.cwd.resolve(
context.invokerRequest.options().altInstallationToolchains().get());
if (!Files.isRegularFile(installationToolchainsFile)) {
@@ -170,7 +170,9 @@ protected void toolchains(MavenContext context,
MavenExecutionRequest request) t
String installationToolchainsFileStr =
context.protoSession.getUserProperties().get(Constants.MAVEN_INSTALLATION_TOOLCHAINS);
if (installationToolchainsFileStr != null) {
- installationToolchainsFile =
context.cwdResolver.apply(installationToolchainsFileStr);
+ installationToolchainsFile = context.installationDirectory
+ .resolve(installationToolchainsFileStr)
+ .normalize();
}
}
@@ -311,10 +313,10 @@ protected void populateRequest(MavenContext context,
Lookup lookup, MavenExecuti
}
protected Path determinePom(MavenContext context, Lookup lookup) {
- Path current = context.invokerRequest.cwd();
+ Path current = context.cwd.get();
MavenOptions options = (MavenOptions) context.invokerRequest.options();
if (options.alternatePomFile().isPresent()) {
- current =
context.cwdResolver.apply(options.alternatePomFile().get());
+ current = context.cwd.resolve(options.alternatePomFile().get());
}
ModelProcessor modelProcessor =
lookup.lookupOptional(ModelProcessor.class).orElse(null);
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
index 2b9d9df83a..6ae551a6a4 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
@@ -78,8 +78,8 @@ protected int execute(EncryptContext context) throws
Exception {
context.addInHeader("This tool is part of Apache Maven 4
distribution.");
context.addInHeader("");
- Thread executeThread = Thread.currentThread();
- context.terminal.handle(Terminal.Signal.INT, signal ->
executeThread.interrupt());
+ context.terminal.handle(
+ Terminal.Signal.INT, signal ->
Thread.currentThread().interrupt());
context.reader =
LineReaderBuilder.builder().terminal(context.terminal).build();
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
index 12abb2eb19..608eeb98b6 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
@@ -20,6 +20,7 @@
import java.nio.file.Path;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.services.Lookup;
@@ -36,12 +37,12 @@
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
-import org.jline.reader.Parser;
import org.jline.reader.Reference;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultHighlighter;
import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.history.DefaultHistory;
+import org.jline.terminal.Terminal;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.InfoCmp;
@@ -67,9 +68,8 @@ protected LookupContext createContext(InvokerRequest
invokerRequest) {
@Override
protected int execute(LookupContext context) throws Exception {
// set up JLine built-in commands
- ConfigurationPath configPath =
- new ConfigurationPath(context.invokerRequest.cwd(),
context.invokerRequest.cwd());
- Builtins builtins = new Builtins(context.invokerRequest::cwd,
configPath, null);
+ ConfigurationPath configPath = new
ConfigurationPath(context.cwd.get(), context.cwd.get());
+ Builtins builtins = new Builtins(context.cwd, configPath, null);
builtins.rename(Builtins.Command.TTOP, "top");
builtins.alias("zle", "widget");
builtins.alias("bindkey", "keymap");
@@ -84,7 +84,8 @@ protected int execute(LookupContext context) throws Exception
{
holder.addCommandRegistry(entry.getValue().createShellCommandRegistry(context));
}
- Parser parser = new DefaultParser();
+ DefaultParser parser = new DefaultParser();
+ parser.setRegexCommand("[:]{0,1}[a-zA-Z!]{1,}\\S*"); // change default
regex to support shell commands
String banner =
"""
@@ -104,10 +105,15 @@ protected int execute(LookupContext context) throws
Exception {
try (holder) {
SimpleSystemRegistryImpl systemRegistry =
- new SimpleSystemRegistryImpl(parser, context.terminal,
context.invokerRequest::cwd, configPath);
+ new SimpleSystemRegistryImpl(parser, context.terminal,
context.cwd, configPath) {
+ @Override
+ public boolean isCommandOrScript(String command) {
+ return command.startsWith("!") ||
super.isCommandOrScript(command);
+ }
+ };
systemRegistry.setCommandRegistries(holder.getCommandRegistries());
- Path history = context.userResolver.apply(".mvnsh_history");
+ Path history = context.userDirectory.resolve(".mvnsh_history");
LineReader reader = LineReaderBuilder.builder()
.terminal(context.terminal)
.history(new DefaultHistory())
@@ -127,16 +133,29 @@ protected int execute(LookupContext context) throws
Exception {
KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
- String prompt = "mvnsh> ";
- String rightPrompt = null;
-
// start the shell and process input until the user quits with
Ctrl-D
- String line;
+ AtomicReference<Exception> failure = new AtomicReference<>();
while (true) {
try {
+ failure.set(null);
systemRegistry.cleanUp();
- line = reader.readLine(prompt, rightPrompt,
(MaskingCallback) null, null);
- systemRegistry.execute(line);
+ Thread commandThread = new Thread(() -> {
+ try {
+ systemRegistry.execute(reader.readLine(
+ context.cwd.get().getFileName().toString()
+ " mvnsh> ",
+ null,
+ (MaskingCallback) null,
+ null));
+ } catch (Exception e) {
+ failure.set(e);
+ }
+ });
+ context.terminal.handle(Terminal.Signal.INT, signal ->
commandThread.interrupt());
+ commandThread.start();
+ commandThread.join();
+ if (failure.get() != null) {
+ throw failure.get();
+ }
} catch (UserInterruptException e) {
// Ignore
// return CANCELED;
@@ -153,7 +172,7 @@ protected int execute(LookupContext context) throws
Exception {
context.writer.accept(context.invokerRequest
.messageBuilderFactory()
.builder()
- .error("Error:" + e.getMessage())
+ .error("Error: " + e.getMessage())
.build());
if
(context.invokerRequest.options().showErrors().orElse(false)) {
e.printStackTrace(context.terminal.writer());
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
index 02ee1e3ce2..54fa219bd2 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
@@ -18,37 +18,44 @@
*/
package org.apache.maven.cling.invoker.mvnsh.builtin;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
-import java.util.EnumSet;
+import java.util.Arrays;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.apache.maven.api.Lifecycle;
+import org.apache.maven.api.cli.InvokerException;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.services.LifecycleRegistry;
+import org.apache.maven.api.services.LookupException;
import org.apache.maven.cling.invoker.LookupContext;
import org.apache.maven.cling.invoker.mvn.MavenInvoker;
import org.apache.maven.cling.invoker.mvn.MavenParser;
import org.apache.maven.cling.invoker.mvnenc.EncryptInvoker;
import org.apache.maven.cling.invoker.mvnenc.EncryptParser;
+import org.apache.maven.cling.invoker.mvnenc.Goal;
import org.apache.maven.cling.invoker.mvnsh.ShellCommandRegistryFactory;
+import org.apache.maven.impl.util.Os;
import org.jline.builtins.Completers;
-import org.jline.builtins.Options;
import org.jline.console.CmdDesc;
import org.jline.console.CommandInput;
import org.jline.console.CommandMethods;
import org.jline.console.CommandRegistry;
-import org.jline.console.impl.AbstractCommandRegistry;
+import org.jline.console.impl.JlineCommandRegistry;
import org.jline.reader.Completer;
import org.jline.reader.impl.completer.ArgumentCompleter;
-import org.jline.reader.impl.completer.NullCompleter;
+import org.jline.reader.impl.completer.StringsCompleter;
import static java.util.Objects.requireNonNull;
-import static
org.jline.console.impl.JlineCommandRegistry.compileCommandOptions;
@Named("builtin")
@Singleton
@@ -57,12 +64,7 @@ public CommandRegistry
createShellCommandRegistry(LookupContext context) {
return new BuiltinShellCommandRegistry(context);
}
- private static class BuiltinShellCommandRegistry extends
AbstractCommandRegistry implements AutoCloseable {
- public enum Command {
- MVN,
- MVNENC
- }
-
+ private static class BuiltinShellCommandRegistry extends
JlineCommandRegistry implements AutoCloseable {
private final LookupContext shellContext;
private final MavenInvoker shellMavenInvoker;
private final MavenParser mavenParser;
@@ -75,15 +77,13 @@ private BuiltinShellCommandRegistry(LookupContext
shellContext) {
this.mavenParser = new MavenParser();
this.shellEncryptInvoker = new
EncryptInvoker(shellContext.invokerRequest.lookup(), contextCopier());
this.encryptParser = new EncryptParser();
- Set<Command> commands = new
HashSet<>(EnumSet.allOf(Command.class));
- Map<Command, String> commandName = new HashMap<>();
- Map<Command, CommandMethods> commandExecute = new HashMap<>();
- for (Command c : commands) {
- commandName.put(c, c.name().toLowerCase());
- }
- commandExecute.put(Command.MVN, new CommandMethods(this::mvn,
this::mvnCompleter));
- commandExecute.put(Command.MVNENC, new
CommandMethods(this::mvnenc, this::mvnencCompleter));
- registerCommands(commandName, commandExecute);
+ Map<String, CommandMethods> commandExecute = new HashMap<>();
+ commandExecute.put("!", new CommandMethods(this::shell,
this::defaultCompleter));
+ commandExecute.put("cd", new CommandMethods(this::cd,
this::cdCompleter));
+ commandExecute.put("pwd", new CommandMethods(this::pwd,
this::defaultCompleter));
+ commandExecute.put("mvn", new CommandMethods(this::mvn,
this::mvnCompleter));
+ commandExecute.put("mvnenc", new CommandMethods(this::mvnenc,
this::mvnencCompleter));
+ registerCommands(commandExecute);
}
private Consumer<LookupContext> contextCopier() {
@@ -130,53 +130,133 @@ public String name() {
return "Builtin Maven Shell commands";
}
- private List<Completers.OptDesc> commandOptions(String command) {
+ private void shell(CommandInput input) {
+ if (input.args().length > 0) {
+ try {
+ ProcessBuilder builder = new ProcessBuilder();
+ List<String> processArgs = new ArrayList<>();
+ if (Os.IS_WINDOWS) {
+ processArgs.add("cmd.exe");
+ processArgs.add("/c");
+ } else {
+ processArgs.add("sh");
+ processArgs.add("-c");
+ }
+ processArgs.add(String.join(" ",
Arrays.asList(input.args())));
+ builder.command(processArgs);
+ builder.directory(shellContext.cwd.get().toFile());
+ Process process = builder.start();
+ Thread out = new Thread(new
StreamGobbler(process.getInputStream(), shellContext.writer));
+ Thread err = new Thread(new
StreamGobbler(process.getErrorStream(), shellContext.logger::error));
+ out.start();
+ err.start();
+ int exitCode = process.waitFor();
+ out.join();
+ err.join();
+ if (exitCode != 0) {
+ shellContext.logger.error("Shell command exited with
code " + exitCode);
+ }
+ } catch (Exception e) {
+ saveException(e);
+ }
+ }
+ }
+
+ private void cd(CommandInput input) {
try {
- invoke(new CommandSession(), command, "--help");
- } catch (Options.HelpException e) {
- return compileCommandOptions(e.getMessage());
+ if (input.args().length == 1) {
+ shellContext.cwd.change(input.args()[0]);
+ } else {
+ shellContext.logger.error("Command accepts only one
argument");
+ }
} catch (Exception e) {
- // ignore
+ saveException(e);
+ }
+ }
+
+ private List<Completer> cdCompleter(String name) {
+ return List.of(new ArgumentCompleter(new
Completers.DirectoriesCompleter(shellContext.cwd)));
+ }
+
+ private void pwd(CommandInput input) {
+ try {
+ shellContext.writer.accept(shellContext.cwd.get().toString());
+ } catch (Exception e) {
+ saveException(e);
}
- return null;
}
private void mvn(CommandInput input) {
try {
shellMavenInvoker.invoke(mavenParser.parseInvocation(
ParserRequest.mvn(input.args(),
shellContext.invokerRequest.messageBuilderFactory())
+ .cwd(shellContext.cwd.get())
.build()));
+ } catch (InvokerException.ExitException e) {
+ shellContext.logger.error("mvn command exited with exit code "
+ e.getExitCode());
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> mvnCompleter(String name) {
- List<Completer> completers = new ArrayList<>();
- completers.add(new ArgumentCompleter(
- NullCompleter.INSTANCE,
- new Completers.OptionCompleter(
- new
Completers.FilesCompleter(shellContext.invokerRequest::cwd),
this::commandOptions, 1)));
- return completers;
+ List<String> names;
+ try {
+ List<String> phases =
shellContext.lookup.lookup(LifecycleRegistry.class).stream()
+ .flatMap(Lifecycle::allPhases)
+ .map(Lifecycle.Phase::name)
+ .toList();
+ // TODO: add goals dynamically
+ List<String> goals = List.of("wrapper:wrapper");
+ names = Stream.concat(phases.stream(),
goals.stream()).toList();
+ } catch (LookupException e) {
+ names = List.of(
+ "clean",
+ "validate",
+ "compile",
+ "test",
+ "package",
+ "verify",
+ "install",
+ "deploy",
+ "wrapper:wrapper");
+ }
+ return List.of(new ArgumentCompleter(new StringsCompleter(names)));
}
private void mvnenc(CommandInput input) {
try {
shellEncryptInvoker.invoke(encryptParser.parseInvocation(
ParserRequest.mvnenc(input.args(),
shellContext.invokerRequest.messageBuilderFactory())
+ .cwd(shellContext.cwd.get())
.build()));
+ } catch (InvokerException.ExitException e) {
+ shellContext.logger.error("mvnenc command exited with exit
code " + e.getExitCode());
} catch (Exception e) {
saveException(e);
}
}
private List<Completer> mvnencCompleter(String name) {
- List<Completer> completers = new ArrayList<>();
- completers.add(new ArgumentCompleter(
- NullCompleter.INSTANCE,
- new Completers.OptionCompleter(
- new
Completers.FilesCompleter(shellContext.invokerRequest::cwd),
this::commandOptions, 1)));
- return completers;
+ return List.of(new ArgumentCompleter(new StringsCompleter(
+ shellContext.lookup.lookupMap(Goal.class).keySet())));
+ }
+ }
+
+ private static class StreamGobbler implements Runnable {
+ private final InputStream inputStream;
+ private final Consumer<String> consumer;
+
+ private StreamGobbler(InputStream inputStream, Consumer<String>
consumer) {
+ this.inputStream = inputStream;
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void run() {
+ new BufferedReader(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))
+ .lines()
+ .forEach(consumer);
}
}
}