Skip to content

Commit

Permalink
Merge pull request #31198 from iocanel/cli-extensions
Browse files Browse the repository at this point in the history
Introduce CLI plugins
  • Loading branch information
maxandersen authored Mar 28, 2023
2 parents d9e2da4 + 5978644 commit 1b34b7c
Show file tree
Hide file tree
Showing 49 changed files with 3,702 additions and 111 deletions.
42 changes: 42 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/CliPlugins.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.quarkus.cli;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import io.quarkus.cli.common.OutputOptionMixin;
import io.quarkus.cli.plugin.CliPluginsAdd;
import io.quarkus.cli.plugin.CliPluginsList;
import io.quarkus.cli.plugin.CliPluginsRemove;
import io.quarkus.cli.plugin.CliPluginsSync;
import picocli.CommandLine;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.Unmatched;

@CommandLine.Command(name = "plugin", aliases = { "plug" }, header = "Configure plugins of the Quarkus CLI.", subcommands = {
CliPluginsList.class,
CliPluginsAdd.class,
CliPluginsRemove.class,
CliPluginsSync.class
})
public class CliPlugins implements Callable<Integer> {

@CommandLine.Mixin(name = "output")
protected OutputOptionMixin output;

@CommandLine.Spec
protected CommandLine.Model.CommandSpec spec;

@Unmatched // avoids throwing errors for unmatched arguments
List<String> unmatchedArgs;

@Override
public Integer call() throws Exception {
output.info("Listing plguins (default action, see --help).");
ParseResult result = spec.commandLine().getParseResult();
List<String> args = result.originalArgs().stream().filter(x -> !"plugin".equals(x) && !"plug".equals(x))
.collect(Collectors.toList());
CommandLine listCommand = spec.subcommands().get("list");
return listCommand.execute(args.toArray(new String[0]));
}
}
89 changes: 89 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,47 @@

import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;

import jakarta.inject.Inject;

import io.quarkus.cli.common.HelpOption;
import io.quarkus.cli.common.OutputOptionMixin;
import io.quarkus.cli.common.PropertiesOptions;
import io.quarkus.cli.plugin.Plugin;
import io.quarkus.cli.plugin.PluginCommandFactory;
import io.quarkus.cli.plugin.PluginManager;
import io.quarkus.cli.plugin.PluginManagerSettings;
import io.quarkus.cli.registry.RegistryClientMixin;
import io.quarkus.cli.utils.Registries;
import io.quarkus.devtools.utils.Prompt;
import io.quarkus.runtime.QuarkusApplication;
import picocli.CommandLine;
import picocli.CommandLine.ExitCode;
import picocli.CommandLine.Help;
import picocli.CommandLine.IHelpSectionRenderer;
import picocli.CommandLine.IParameterExceptionHandler;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.UsageMessageSpec;
import picocli.CommandLine.MutuallyExclusiveArgsException;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.ScopeType;
import picocli.CommandLine.UnmatchedArgumentException;

@CommandLine.Command(name = "quarkus", subcommands = {
Create.class, Build.class, Dev.class, Test.class, ProjectExtensions.class, Image.class, Deploy.class, Registry.class,
Info.class,
Update.class,
Version.class,
CliPlugins.class,
Completion.class }, scope = ScopeType.INHERIT, sortOptions = false, showDefaultValues = true, versionProvider = Version.class, subcommandsRepeatable = false, mixinStandardHelpOptions = false, commandListHeading = "%nCommands:%n", synopsisHeading = "%nUsage: ", optionListHeading = "Options:%n", headerHeading = "%n", parameterListHeading = "%n")
public class QuarkusCli implements QuarkusApplication, Callable<Integer> {
static {
Expand All @@ -35,6 +52,9 @@ public class QuarkusCli implements QuarkusApplication, Callable<Integer> {
@Inject
CommandLine.IFactory factory;

@CommandLine.Mixin
protected RegistryClientMixin registryClient;

@CommandLine.Mixin
protected HelpOption helpOption;

Expand All @@ -56,9 +76,62 @@ public int run(String... args) throws Exception {
CommandLine cmd = factory == null ? new CommandLine(this) : new CommandLine(this, factory);
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());

//When running tests the cli should not prompt for user input.
boolean interactiveMode = Arrays.stream(args).noneMatch(arg -> arg.equals("--cli-test"));
PluginCommandFactory pluginCommandFactory = new PluginCommandFactory(output);
PluginManager pluginManager = pluginManager(output, interactiveMode);
pluginManager.syncIfNeeded();
Map<String, Plugin> plugins = new HashMap<>(pluginManager.getInstalledPlugins());
pluginCommandFactory.populateCommands(cmd, plugins);
try {
Optional<String> missing = checkMissingCommand(cmd, args);
missing.ifPresent(m -> {
Map<String, Plugin> installable = pluginManager.getInstallablePlugins();
if (installable.containsKey(m)) {
if (interactiveMode && Prompt.yesOrNo(true,
"Command %s is not installed, but a matching plugin is available. Would you like to install it now ?",
args)) {
pluginManager.addPlugin(m).ifPresent(added -> plugins.put(added.getName(), added));
pluginCommandFactory.populateCommands(cmd, plugins);
}
}
});
} catch (MutuallyExclusiveArgsException e) {
return ExitCode.USAGE;
}
return cmd.execute(args);
}

/**
* Recursivelly processes the arguments passed to the command and checks wether a subcommand is missing.
*
* @param root the root command
* @param args the arguments passed to the root command
* @retunr the missing subcommand wrapped in {@link Optional} or empty if no subcommand is missing.
*/
public Optional<String> checkMissingCommand(CommandLine root, String[] args) {
if (args.length == 0) {
return Optional.empty();
}

try {
ParseResult result = root.parseArgs(args);
if (args.length == 1) {
return Optional.empty();
}
CommandLine next = root.getSubcommands().get(args[0]);
if (next == null) {
return Optional.of(args[0]);
}
String[] remaining = new String[args.length - 1];
System.arraycopy(args, 1, remaining, 0, remaining.length);
return checkMissingCommand(next, remaining).map(nextMissing -> root.getCommandName() + "-" + nextMissing);
} catch (UnmatchedArgumentException e) {
return Optional.of(args[0]);
}
}

@Override
public Integer call() throws Exception {
output.info("%n@|bold Quarkus CLI|@ version %s", Version.clientVersion());
Expand Down Expand Up @@ -147,4 +220,20 @@ private String description(UsageMessageSpec usageMessage) {
}
}

private static Optional<Path> getProjectRoot(OutputOptionMixin output) {
Path projectRoot = output != null ? output.getTestDirectory() : null;
if (projectRoot == null) {
projectRoot = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
}
return Optional.ofNullable(projectRoot);
}

private PluginManager pluginManager(OutputOptionMixin output, boolean interactiveMode) {
PluginManagerSettings settings = PluginManagerSettings.defaultSettings()
.withCatalogs(Registries.getRegistries(registryClient, "quarkusio"))
.withInteractivetMode(interactiveMode); // Why not just getting it from output.isClieTest ? Cause args have not been parsed yet.

return new PluginManager(settings, output, Optional.ofNullable(Paths.get(System.getProperty("user.home"))),
getProjectRoot(output), Optional.empty(), p -> true);
}
}
59 changes: 10 additions & 49 deletions devtools/cli/src/main/java/io/quarkus/cli/build/ExecuteUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,30 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import io.quarkus.cli.common.OutputOptionMixin;
import io.quarkus.utilities.OS;
import io.quarkus.devtools.exec.ExecSupport;
import io.quarkus.devtools.exec.Executable;

public class ExecuteUtil {

public static File findExecutableFile(String base) {
String path = null;
String executable = base;
private static ExecSupport withOutput(OutputOptionMixin output) {
return new ExecSupport(output.out(), output.err(), output.isVerbose(), output.isCliTest());
}

if (OS.determineOS() == OS.WINDOWS) {
executable = base + ".cmd";
path = findExecutable(executable);
if (path == null) {
executable = base + ".bat";
path = findExecutable(executable);
}
} else {
executable = base;
path = findExecutable(executable);
}
if (path == null)
return null;
return new File(path, executable);
public static File findExecutableFile(String base) {
return Executable.findExecutableFile(base);
}

private static String findExecutable(String exec) {
return Stream.of(System.getenv("PATH").split(Pattern.quote(File.pathSeparator))).map(Paths::get)
.map(path -> path.resolve(exec).toFile()).filter(File::exists).findFirst().map(File::getParent)
.orElse(null);
return Executable.findExecutable(exec);
}

public static File findExecutable(String name, String errorMessage, OutputOptionMixin output) {
File command = ExecuteUtil.findExecutableFile(name);
if (command == null) {
output.error(errorMessage);
throw new RuntimeException("Unable to find " + name + " command");
}
return command;
return Executable.findExecutable(name, errorMessage, output);
}

public static int executeProcess(OutputOptionMixin output, String[] args, File parentDir)
Expand Down Expand Up @@ -102,24 +81,6 @@ public static int executeProcess(OutputOptionMixin output, String[] args, File p
}

public static File findWrapper(Path projectRoot, String[] windows, String other) {
if (OS.determineOS() == OS.WINDOWS) {
for (String name : windows) {
File wrapper = new File(projectRoot + File.separator + name);
if (wrapper.isFile())
return wrapper;
}
} else {
File wrapper = new File(projectRoot + File.separator + other);
if (wrapper.isFile())
return wrapper;
}

// look for a wrapper in a parent directory
Path normalizedPath = projectRoot.normalize();
if (!normalizedPath.equals(projectRoot.getRoot())) {
return findWrapper(normalizedPath.getParent(), windows, other);
} else {
return null;
}
return Executable.findWrapper(projectRoot, windows, other);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.quarkus.cli.plugin;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.Callable;

import io.quarkus.cli.common.RunModeOption;
import picocli.CommandLine;

@CommandLine.Command(name = "add", header = "Add plugin(s) to the Quarkus CLI.")
public class CliPluginsAdd extends CliPluginsBase implements Callable<Integer> {

@CommandLine.Mixin
RunModeOption runMode;

@CommandLine.Option(names = { "-d",
"--description" }, paramLabel = "Plugin description", order = 5, description = "The plugin description")
Optional<String> description;

@CommandLine.Parameters(arity = "1", paramLabel = "PLUGIN_NAME", description = " The plugin name or location (e.g. url, path or maven coordinates in GACTV form)")
String nameOrLocation;

@Override
public Integer call() {
try {
output.debug("Add plugin with initial parameters: %s", this);
output.throwIfUnmatchedArguments(spec.commandLine());

if (runMode.isDryRun()) {
dryRunAdd(spec.commandLine().getHelp());
return CommandLine.ExitCode.OK;
}

return addPlugin();
} catch (Exception e) {
return output.handleCommandException(e,
"Unable to add plugin(s): " + nameOrLocation + " of type: " + type.map(PluginType::name).orElse("<any>")
+ "."
+ e.getMessage());
}
}

Integer addPlugin() throws IOException {
PluginManager pluginManager = pluginManager();
Optional<Plugin> addedPlugin = pluginManager.addPlugin(nameOrLocation, description);

return addedPlugin.map(plugin -> {
PluginListTable table = new PluginListTable(List.of(new PluginListItem(true, plugin)), false);
output.info("Added plugin:");
output.info(table.getContent());
return CommandLine.ExitCode.OK;
}).orElseGet(() -> {
output.error("No plugin available at: " + this.nameOrLocation);
printHints(true);
return CommandLine.ExitCode.USAGE;
});
}

private void printHints(boolean pluginListHint) {
if (runMode.isBatchMode())
return;

if (pluginListHint) {
output.info("To see the list of installable plugins, use the 'plugin list' subcommand.");
}
}

void dryRunAdd(CommandLine.Help help) {
output.printText(new String[] {
"\nAdd plugin to the CLI\n",
"\t" + projectRoot().toString()
});
Map<String, String> dryRunOutput = new TreeMap<>();
dryRunOutput.put("Name or Location", nameOrLocation);
type.ifPresent(t -> dryRunOutput.put("Type", t.name()));
output.info(help.createTextTable(dryRunOutput).toString());
};
}
Loading

0 comments on commit 1b34b7c

Please sign in to comment.