Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added support for "plugins" #1000

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions src/main/java/dev/jbang/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import java.util.ArrayList;
import java.util.List;

import dev.jbang.catalog.Alias;
import dev.jbang.catalog.Catalog;
import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.JBang;
import dev.jbang.util.Util;

import picocli.CommandLine;

Expand All @@ -29,15 +33,46 @@ private static String[] handleDefaultRun(CommandLine.Model.CommandSpec spec, Str
leadingOpts.add(arg);
}
}
// Check if we have a parameter and it's not the same as any of the subcommand
// names
if (!remainingArgs.isEmpty() && !spec.subcommands().containsKey(remainingArgs.get(0))
|| hasRunOpts(leadingOpts)) {
// Check if we have a parameter, and it's not the same as any of the
// subcommand names
if (hasRunOpts(leadingOpts)) {
List<String> result = new ArrayList<>();
result.add("run");
result.addAll(leadingOpts);
result.addAll(remainingArgs);
args = result.toArray(args);
} else if (!remainingArgs.isEmpty()) {
String cmd = remainingArgs.get(0);
if (!spec.subcommands().containsKey(cmd)) {
if (Catalog.isValidName(cmd) && Alias.get("jbang-" + cmd) != null) {
// We found a matching "jbang-xxx" alias
remainingArgs.set(0, "jbang-" + cmd);
} else if (Alias.get(cmd) != null) {
// We found an exactly matching alias
// We do this test because we want aliases to have a higher
// priority than the next case, which is to look up commands
// in the user's PATH which might be slow-ish
} else if (!Util.findCommandsWith(p -> Util.base(p.getFileName().toString()).equals("jbang-" + cmd))
.isEmpty()) {
// We found a matching "jbang-xxx" command on the user's PATH
List<String> result = new ArrayList<>();
result.add("jbang-" + cmd);
result.addAll(leadingOpts);
result.add("--");
result.addAll(remainingArgs.subList(1, remainingArgs.size()));
String cmdLine = String.join(" ", result);
Util.verboseMsg("run plugin: " + cmdLine);
System.out.println(cmdLine);
System.exit(BaseCommand.EXIT_EXECUTE);
} else {
// In all other cases assume it's an implicit "run" (no need to do anything)
}
List<String> result = new ArrayList<>();
result.add("run");
result.addAll(leadingOpts);
result.addAll(remainingArgs);
args = result.toArray(args);
}
}
return args;
}
Expand Down
76 changes: 61 additions & 15 deletions src/main/java/dev/jbang/cli/JBang.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static picocli.CommandLine.ScopeType;

import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
Expand All @@ -21,6 +22,7 @@
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Future;

import dev.jbang.Configuration;
Expand Down Expand Up @@ -239,15 +241,46 @@ public static CommandGroupRenderer getCommandRenderer() {
sections.put("Caching", asList("cache", "export", "jdk"));
sections.put("Configuration", asList("config", "trust", "alias", "template", "catalog", "app"));
sections.put("Other", asList("completion", "info", "version", "wrapper"));
CommandGroupRenderer renderer = new CommandGroupRenderer(sections);
Map<String, String> cmds = findExternalCommands();
if (!cmds.isEmpty()) {
sections.put("External", asList());
}
CommandGroupRenderer renderer = new CommandGroupRenderer(sections, cmds);
return renderer;
}

private static Map<String, String> findExternalCommands() {
Map<String, String> result = new TreeMap<>();
// Add any aliases whose names start with "jbang-" to the result
try {
dev.jbang.catalog.Catalog cat = dev.jbang.catalog.Catalog.getMerged(false);
for (String name : cat.aliases.keySet()) {
if (name.startsWith("jbang-")) {
result.put(name.substring(6), cat.aliases.get(name).description);
}
}
} catch (Exception ex) {
Util.verboseMsg("Error trying to list aliases", ex);
}
// Now add any commands found on the PATH whose names start with "jbang-"
try {
List<Path> cmds = Util.findCommandsWith(p -> p.getFileName().toString().startsWith("jbang-"));
for (Path p : cmds) {
result.put(Util.base(p.getFileName().toString()).substring(6), null);
}
} catch (Exception ex) {
Util.verboseMsg("Error trying to list jbang-commands", ex);
}
return result;
}

public static class CommandGroupRenderer implements CommandLine.IHelpSectionRenderer {
private final Map<String, List<String>> sections;
private final Map<String, String> externals;

public CommandGroupRenderer(Map<String, List<String>> sections) {
public CommandGroupRenderer(Map<String, List<String>> sections, Map<String, String> externals) {
this.sections = sections;
this.externals = externals;
}

/**
Expand Down Expand Up @@ -293,23 +326,26 @@ public String render(CommandLine.Help help) {
private String renderSection(String sectionHeading, List<String> cmdNames, CommandLine.Help help) {
TextTable textTable = createTextTable(help);

for (String name : cmdNames) {
CommandSpec sub = help.commandSpec().subcommands().get(name).getCommandSpec();

// create comma-separated list of command name and aliases
String names = sub.names().toString();
names = names.substring(1, names.length() - 1); // remove leading '[' and trailing ']'
if (!sectionHeading.equals("External")) {
for (String name : cmdNames) {
CommandSpec sub = help.commandSpec().subcommands().get(name).getCommandSpec();

// description may contain line separators; use Text::splitLines to handle this
String description = description(sub.usageMessage());
CommandLine.Help.Ansi.Text[] lines = help.colorScheme().text(description).splitLines();
// create comma-separated list of command name and aliases
String names = sub.names().toString();
names = names.substring(1, names.length() - 1); // remove leading '[' and trailing ']'

for (int i = 0; i < lines.length; i++) {
CommandLine.Help.Ansi.Text cmdNamesText = help.colorScheme().commandText(i == 0 ? names : "");
textTable.addRowValues(cmdNamesText, lines[i]);
// description may contain line separators; use Text::splitLines to handle this
String description = description(sub.usageMessage());
addCommand(textTable, names, description, help);
}
} else {
for (String name : externals.keySet()) {
String description = externals.get(name);
addCommand(textTable, name, description, help);
}
}
return help.createHeading("%n" + sectionHeading + ":%n") + textTable.toString();

return help.createHeading("%n" + sectionHeading + ":%n") + textTable;
}

private TextTable createTextTable(CommandLine.Help help) {
Expand Down Expand Up @@ -343,5 +379,15 @@ private String description(UsageMessageSpec usageMessage) {
}
return "";
}

private void addCommand(TextTable textTable, String name, String description, CommandLine.Help help) {
CommandLine.Help.Ansi.Text[] lines = help .colorScheme()
.text(description != null ? description : "")
.splitLines();
for (int i = 0; i < lines.length; i++) {
CommandLine.Help.Ansi.Text cmdNamesText = help.colorScheme().commandText(i == 0 ? name : "");
textTable.addRowValues(cmdNamesText, lines[i]);
}
}
}
}
47 changes: 42 additions & 5 deletions src/main/java/dev/jbang/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -238,7 +241,7 @@ public static String getBaseName(String fileName) {
/**
* Returns the name without extension. Will return the name itself if it has no
* extension
*
*
* @param name A file name
* @return A name without extension
*/
Expand Down Expand Up @@ -268,7 +271,7 @@ public static String sourceBase(String name) {
* Returns the extension of the given file name. The extension will not include
* the dot as part of the result. Returns an empty string if the name has no
* extension.
*
*
* @param name A file name
* @return An extension or an empty string
*/
Expand Down Expand Up @@ -1486,29 +1489,42 @@ public static Optional<String> getSourcePackage(String content) {
}

/**
* Searches the locations defined by PATH for the given executable
* Searches the locations defined by `PATH` for the given executable
*
* @param cmd The name of the executable to look for
* @return A Path to the executable, if found, null otherwise
*/
public static Path searchPath(String cmd) {
String envPath = System.getenv("PATH");
envPath = envPath != null ? envPath : "";
return searchPath(cmd, envPath);
return searchPath(cmd, envPath, p -> true);
}

/**
* Searches the locations defined by `paths` for the given executable
* Searches the given `paths` for the given executable
*
* @param cmd The name of the executable to look for
* @param paths A string containing the paths to search
* @return A Path to the executable, if found, null otherwise
*/
public static Path searchPath(String cmd, String paths) {
return searchPath(cmd, paths, p -> true);
}

/**
* Searches the locations defined by `paths` for the given executable
*
* @param cmd The name of the executable to look for
* @param paths A string containing the paths to search
* @param pathFilter User filter for the executables found
* @return A Path to the executable, if found, null otherwise
*/
public static Path searchPath(String cmd, String paths, Predicate<Path> pathFilter) {
return Arrays .stream(paths.split(File.pathSeparator))
.map(dir -> Paths.get(dir).resolve(cmd))
.flatMap(Util::executables)
.filter(Util::isExecutable)
.filter(pathFilter)
.findFirst()
.orElse(null);
}
Expand Down Expand Up @@ -1754,4 +1770,25 @@ public static <T> List<T> join(Collection<T>... lists) {
public static <K, V> Entry<K, V> entry(K k, V v) {
return new AbstractMap.SimpleEntry<K, V>(k, v);
}

public static List<Path> findCommandsWith(Predicate<Path> accept) {
String[] elems = System.getenv().getOrDefault("PATH", "").split(File.pathSeparator);
return Stream
.of(elems)
.map(elem -> Util.getCwd().resolve(elem))
.flatMap(dir -> listFiles(dir).filter(p -> isExecutable(p)).filter(accept))
.collect(Collectors.toList());
}

private static Stream<Path> listFiles(Path dir) {
if (Files.isDirectory(dir)) {
try {
return Files.list(dir);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
} else {
return Stream.empty();
}
}
}