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); } } }