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 <ta...@cservenak.net>
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);
         }
     }
 }


Reply via email to