dev.jbang.cli.App Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jbang-cli Show documentation
Show all versions of jbang-cli Show documentation
JBang Command Line Interface
package dev.jbang.cli;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.jbang.Cache;
import dev.jbang.Settings;
import dev.jbang.catalog.CatalogUtil;
import dev.jbang.dependencies.DependencyUtil;
import dev.jbang.net.JdkManager;
import dev.jbang.net.JdkProvider;
import dev.jbang.source.Project;
import dev.jbang.source.ProjectBuilder;
import dev.jbang.util.CommandBuffer;
import dev.jbang.util.UnpackUtil;
import dev.jbang.util.Util;
import picocli.CommandLine;
@CommandLine.Command(name = "app", description = "Manage scripts installed on the user's PATH as commands.", subcommands = {
AppInstall.class, AppList.class,
AppUninstall.class, AppSetup.class })
public class App {
public static void deleteCommandFiles(String name) {
try (Stream files = Files.list(Settings.getConfigBinDir())) {
files .filter(f -> f.getFileName().toString().equals(name)
|| f.getFileName().toString().startsWith(name + "."))
.forEach(f -> Util.deletePath(f, true));
} catch (IOException e) {
// Ignore
}
}
}
@CommandLine.Command(name = "install", description = "Install a script as a command.")
class AppInstall extends BaseCommand {
private static final String jbangUrl = "https://www.jbang.dev/releases/latest/download/jbang.zip";
@CommandLine.Option(names = {
"--force" }, description = "Force re-installation")
boolean force;
@CommandLine.Option(names = { "--name" }, description = "A name for the command")
String name;
@CommandLine.Mixin
ScriptMixin scriptMixin;
@CommandLine.Mixin
BuildMixin buildMixin;
@CommandLine.Mixin
DependencyInfoMixin dependencyInfoMixin;
@CommandLine.Mixin
NativeMixin nativeMixin;
@CommandLine.Mixin
JdkProvidersMixin jdkProvidersMixin;
@CommandLine.Mixin
RunMixin runMixin;
@CommandLine.Option(names = { "--enable-preview" }, description = "Activate Java preview features")
Boolean enablePreviewRequested;
@CommandLine.Parameters(index = "1..*", arity = "0..*", description = "Parameters to pass on to the script")
public List userParams = new ArrayList<>();
@Override
public Integer doCall() {
scriptMixin.validate();
boolean installed = false;
try {
if (scriptMixin.scriptOrFile.equals("jbang")) {
if (name != null && !"jbang".equals(name)) {
throw new IllegalArgumentException(
"It's not possible to install jbang with a different name");
}
installed = installJBang(force);
} else {
if ("jbang".equals(name)) {
throw new IllegalArgumentException("jbang is a reserved name.");
}
if (name != null && !CatalogUtil.isValidName(name)) {
throw new IllegalArgumentException("Not a valid command name: '" + name + "'");
}
List runOpts = collectRunOptions();
installed = install(name, scriptMixin.scriptOrFile, force, runOpts, userParams);
}
if (installed) {
if (AppSetup.needsSetup()) {
return AppSetup.setup(AppSetup.guessWithJava(), false, false);
}
}
} catch (IOException e) {
throw new ExitException(EXIT_INTERNAL_ERROR, "Could not install command", e);
}
return EXIT_OK;
}
private List collectRunOptions() {
List opts = new ArrayList<>();
opts.addAll(scriptMixin.opts());
opts.addAll(buildMixin.opts());
opts.addAll(dependencyInfoMixin.opts());
opts.addAll(nativeMixin.opts());
opts.addAll(jdkProvidersMixin.opts());
opts.addAll(runMixin.opts());
if (Boolean.TRUE.equals(enablePreviewRequested)) {
opts.add("--enable-preview");
}
return opts;
}
public static boolean install(String name, String scriptRef, boolean force, List runOpts,
List runArgs) throws IOException {
Path binDir = Settings.getConfigBinDir();
if (!force && name != null && existScripts(binDir, name)) {
Util.infoMsg("A script with name '" + name + "' already exists, use '--force' to install anyway.");
return false;
}
ProjectBuilder pb = Project.builder();
Project prj = pb.build(scriptRef);
if (name == null) {
name = CatalogUtil.nameFromRef(scriptRef);
if (!force && existScripts(binDir, name)) {
Util.infoMsg("A script with name '" + name + "' already exists, use '--force' to install anyway.");
return false;
}
}
if (!pb.isAlias(prj.getResourceRef()) && !DependencyUtil.looksLikeAGav(scriptRef)
&& !prj.getResourceRef().isURL()) {
scriptRef = prj.getResourceRef().getFile().toAbsolutePath().toString();
}
prj.codeBuilder().build();
installScripts(name, scriptRef, runOpts, runArgs);
Util.infoMsg("Command installed: " + name);
return true;
}
private static boolean existScripts(Path binDir, String name) {
return Files.exists(binDir.resolve(name)) || Files.exists(binDir.resolve(name + ".cmd"))
|| Files.exists(binDir.resolve(name + ".ps1"));
}
private static void installScripts(String name, String scriptRef, List runOpts, List runArgs)
throws IOException {
Path binDir = Settings.getConfigBinDir();
binDir.toFile().mkdirs();
if (Util.isWindows()) {
installCmdScript(binDir.resolve(name + ".cmd"), scriptRef, runOpts, runArgs);
installPSScript(binDir.resolve(name + ".ps1"), scriptRef, runOpts, runArgs);
// Script references on Linux/Mac should never contain backslashes
String nixRef = scriptRef.replace('\\', '/');
installShellScript(binDir.resolve(name), nixRef, runOpts, runArgs);
} else {
installShellScript(binDir.resolve(name), scriptRef, runOpts, runArgs);
}
}
private static void installShellScript(Path file, String scriptRef, List runOpts, List runArgs)
throws IOException {
List cmd = new ArrayList<>();
cmd.addAll(Arrays.asList("exec", "jbang", "run"));
cmd.addAll(runOpts);
cmd.add(scriptRef);
cmd.addAll(runArgs);
CommandBuffer cb = CommandBuffer.of(cmd);
List lines = Arrays.asList("#!/bin/sh", cb.asCommandLine(Util.Shell.bash) + " \"$@\"");
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
if (!Util.isWindows()) {
setExecutable(file);
}
}
private static void setExecutable(Path file) {
final Set permissions;
try {
permissions = Files.getPosixFilePermissions(file);
permissions.add(PosixFilePermission.OWNER_EXECUTE);
permissions.add(PosixFilePermission.GROUP_EXECUTE);
Files.setPosixFilePermissions(file, permissions);
} catch (UnsupportedOperationException | IOException e) {
throw new ExitException(EXIT_GENERIC_ERROR, "Couldn't mark script as executable: " + file, e);
}
}
private static void installCmdScript(Path file, String scriptRef, List runOpts, List runArgs)
throws IOException {
List cmd = new ArrayList<>();
cmd.addAll(Arrays.asList("jbang", "run"));
cmd.addAll(runOpts);
cmd.add(scriptRef);
cmd.addAll(runArgs);
CommandBuffer cb = CommandBuffer.of(cmd);
List lines = Arrays.asList("@echo off", cb.asCommandLine(Util.Shell.cmd) + " %*");
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
}
private static void installPSScript(Path file, String scriptRef, List runOpts, List runArgs)
throws IOException {
List cmd = new ArrayList<>();
cmd.addAll(Arrays.asList("jbang", "run"));
cmd.addAll(runOpts);
cmd.add(scriptRef);
cmd.addAll(runArgs);
CommandBuffer cb = CommandBuffer.of(cmd);
List lines = Collections.singletonList(cb.asCommandLine(Util.Shell.powershell) + " @args");
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
}
public static boolean installJBang(boolean force) throws IOException {
Path binDir = Settings.getConfigBinDir();
boolean managedJBang = Files.exists(binDir.resolve("jbang.jar"));
if (!force && (managedJBang || Util.searchPath("jbang") != null)) {
Util.infoMsg("jbang is already available, re-run with --force to install anyway.");
return false;
}
if (force || !managedJBang) {
if (!Util.isOffline()) {
Util.withCacheEvict(() -> {
// Download JBang and unzip to ~/.jbang/bin/
Util.infoMsg("Downloading and installing jbang...");
Path zipFile = Util.downloadAndCacheFile(jbangUrl);
Path urlsDir = Settings.getCacheDir(Cache.CacheClass.urls);
Util.deletePath(urlsDir.resolve("jbang"), true);
UnpackUtil.unpack(zipFile, urlsDir);
App.deleteCommandFiles("jbang");
Path fromDir = urlsDir.resolve("jbang").resolve("bin");
copyJBangFiles(fromDir, binDir);
return 0;
});
} else {
Path jar = Util.getJarLocation();
if (!jar.toString().endsWith(".jar")) {
throw new ExitException(EXIT_GENERIC_ERROR, "Could not determine jbang location");
}
Path fromDir = jar.getParent();
if (fromDir.endsWith(".jbang")) {
fromDir = fromDir.getParent();
}
copyJBangFiles(fromDir, binDir);
}
} else {
Util.infoMsg("jbang is already installed.");
}
return true;
}
private static void copyJBangFiles(Path from, Path to) throws IOException {
to.toFile().mkdirs();
Stream .of("jbang", "jbang.cmd", "jbang.ps1", "jbang.jar")
.map(Paths::get)
.forEach(f -> {
try {
Path fromp = from.resolve(f);
Path top = to.resolve(f);
if (f.endsWith("jbang.jar")) {
if (!Files.isReadable(fromp)) {
fromp = from.resolve(".jbang/jbang.jar");
}
if (Util.isWindows() && Files.isRegularFile(top)) {
top = to.resolve("jbang.jar.new");
}
}
Files.copy(fromp, top, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
} catch (IOException e) {
throw new ExitException(EXIT_GENERIC_ERROR, "Could not copy " + f.toString(), e);
}
});
}
}
@CommandLine.Command(name = "list", description = "Lists installed commands.")
class AppList extends BaseCommand {
@CommandLine.Mixin
FormatMixin formatMixin;
@Override
public Integer doCall() {
if (formatMixin.format == FormatMixin.Format.json) {
Gson parser = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
parser.toJson(listCommandFiles(), System.out);
} else {
listCommandFiles().forEach(app -> System.out.println(app.name));
}
return EXIT_OK;
}
static class AppOut {
String name;
public String getName() {
return name;
}
public AppOut(Path file) {
name = Util.base(file.getFileName().toString());
}
}
private static List listCommandFiles() {
try (Stream files = Files.list(Settings.getConfigBinDir())) {
return files
.filter(Files::isExecutable)
.sorted()
.map(AppOut::new)
.filter(distinctByKey(AppOut::getName))
.collect(Collectors.toList());
} catch (IOException e) {
return Collections.emptyList();
}
}
private static Predicate distinctByKey(Function super T, ?> keyExtractor) {
Set