Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

scripts/mise_shell.java

Raw
import jdk.jshell.*;
import java.util.*;

final int USAGE_EXIT = 64;
final int FAILURE_EXIT = 1;
final String LOCAL_ENGINE = "local";

/// Tiny Java shell for mise inline tasks.
///
/// Mise passes each task command as one argv item. This evaluates that item as
/// JShell snippets in the local JVM and exits non-zero on Java errors or thrown exceptions.
void main(String[] args) throws Exception
{
	if (args.length != 1)
	{
		System.err.println("expected one mise task snippet arg, got " + args.length);
		System.exit(USAGE_EXIT);
	}

	try (var jshell = JShell.builder().executionEngine(LOCAL_ENGINE).out(System.out).err(System.err).build())
	{
		if (!evalAll(jshell, startup(), true) || !evalAll(jshell, args[0], false))
			System.exit(FAILURE_EXIT);
	}
}

/// Evaluate a possibly multi-snippet Java task.
///
/// Reason: `JShell.eval(...)` only reliably evaluates one complete snippet at a
/// time. Mise tasks are often multi-line blocks, so this uses JShell's parser to
/// split complete snippets before execution.
///
/// Usage: first evaluate `startup()` with `startup = true`, then the task body
/// with `startup = false`.
///
/// Pitfall: state lives only inside this Java process. A mise `run = [...]` array
/// starts separate shell processes, so variables and `cd(...)` do not cross array
/// entries.
boolean evalAll(JShell jshell, String input, boolean startup)
{
	for (var rest = input; !rest.isBlank(); )
	{
		var info = jshell.sourceCodeAnalysis().analyzeCompletion(rest);
		if (!info.completeness().isComplete())
		{
			System.err.println(prefix(startup) + "incomplete snippet: " + info.completeness());
			return false;
		}
		if (!evalOne(jshell, info.source(), startup))
			return false;
		rest = info.remaining();
	}
	return true;
}

/// Evaluate one complete JShell snippet and translate JShell status into exit status.
///
/// Reason: the `jshell` CLI exits `0` for some rejected snippets. Mise tasks need
/// hard failure on compiler errors and thrown exceptions.
///
/// Usage: called by `evalAll(...)` after completeness analysis.
///
/// Pitfall: diagnostics point at JShell's synthetic snippet, not a physical
/// `mise.toml` line number. Keep tasks short and direct.
boolean evalOne(JShell jshell, String code, boolean startup)
{
	var ok = true;
	for (var event : jshell.eval(code))
	{
		if (event.exception() != null)
		{
			var message = event.exception().getMessage();
			System.err.println(message == null ? event.exception().toString() : message);
			ok = false;
		}
		if (rejected(event.status()))
		{
			jshell.diagnostics(event.snippet()).forEach(d -> System.err.println(prefix(startup) + d.getMessage(Locale.ROOT)));
			ok = false;
		}
	}
	return ok;
}

boolean rejected(Snippet.Status status)
{
	return status == Snippet.Status.REJECTED || status == Snippet.Status.RECOVERABLE_NOT_DEFINED;
}

String prefix(boolean startup)
{
	return startup ? "startup: " : "";
}

/// Built-in imports and shell-like helpers exposed to every mise task.
///
/// Reason: root dorkfiles tasks should stay Java, terse, and cross-platform
/// without depending on Bash/Nushell syntax.
///
/// Usage: task bodies can call helpers directly, e.g.
/// `run("java", "bootstripper.java", "check");`.
///
/// Pitfall: this is startup source code for JShell. Syntax errors here break
/// every task before the task body runs.
String startup()
{
	return String.join("\n",
		imports(),
		stateHelpers(),
		resultHelpers(),
		pathHelpers(),
		envHelpers(),
		processHelpers(),
		fileHelpers(),
		deleteHelpers(),
		globHelpers(),
		whichHelpers()
	);
}

String imports()
{
	return """
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;
""";
}

String stateHelpers()
{
	return """
Path CWD = Path.of("").toAbsolutePath().normalize();
Map<String, String> ENV = new HashMap<>(System.getenv());
""";
}

String resultHelpers()
{
	return """
/// Captured process result.
///
/// Reason: `run(...)` is fail-fast and streams output. Some tasks need captured
/// text or an exit code instead.
///
/// Usage:
/// `var r = result("git", "status", "--short");`
/// `if (r.code() != 0) throw new RuntimeException(r.out());`
///
/// Pitfall: stdout and stderr are merged, matching typical task logs but not
/// preserving separate streams.
record Result(int code, String out)
{
	/// Return captured process output unchanged.
	///
	/// Reason: terse alias when reading command output.
	///
	/// Usage: `IO.print(result("git", "status").text());`.
	///
	/// Pitfall: includes trailing newlines exactly as emitted.
	String text() { return out; }

	/// Return captured output stripped of leading/trailing whitespace.
	///
	/// Reason: common for command substitutions like `git rev-parse`.
	///
	/// Usage: `var head = result("git", "rev-parse", "HEAD").trimmed();`.
	///
	/// Pitfall: stripping can destroy meaningful whitespace; use `text()` for raw data.
	String trimmed() { return out.strip(); }

	/// Return captured output split into lines.
	///
	/// Reason: Java replacement for shell pipelines that iterate line output.
	///
	/// Usage: `for (var file : result("git", "ls-files").lines()) IO.println(file);`.
	///
	/// Pitfall: uses Java `String.lines()`, so line terminators are removed.
	List<String> lines() { return out.lines().toList(); }
}
""";
}

String pathHelpers()
{
	return """
/// Return the task working directory.
///
/// Reason: `cd(...)` is local state inside this Java shell, not OS process state.
/// This exposes that state clearly.
///
/// Usage: `IO.println(pwd());`.
///
/// Pitfall: mise `dir = "..."` sets the initial process directory before this
/// shell starts; `pwd()` reports that effective directory.
Path pwd()
{
	return CWD;
}

/// Resolve a path against `pwd()`.
///
/// Reason: shell tasks constantly need repo-relative paths. This avoids string
/// concatenation and normalizes `.` / `..`.
///
/// Usage: `Files.exists(path("scripts", "mise_shell.java"));`.
///
/// Pitfall: absolute `first` values stay absolute because `Path.resolve(...)`
/// follows Java NIO rules. Prefer relative paths in repo tasks.
Path path(String first, String... more)
{
	return CWD.resolve(Path.of(first, more)).toAbsolutePath().normalize();
}

/// Change this shell's working directory.
///
/// Reason: Java cannot change the OS parent process directory, but this wrapper
/// can keep a `CWD` used by `run(...)` and file helpers.
///
/// Usage: `cd("pi"); run("mise", "run", "check");`.
///
/// Pitfall: state lasts only for the current mise command. It does not cross
/// separate `run = [...]` entries or dependent tasks.
void cd(String dir)
{
	var next = path(dir);
	if (!Files.isDirectory(next))
		throw new IllegalArgumentException("not a directory: " + next);
	CWD = next;
}
""";
}

String envHelpers()
{
	return """
/// Read a task environment variable from this shell's environment map.
///
/// Reason: tasks need access to mise `env = { ... }` values without shell syntax.
///
/// Usage: `IO.println(env("HOSTNAME"));`.
///
/// Pitfall: returns `null` when absent; check required values explicitly.
String env(String name)
{
	return ENV.get(name);
}

/// Set an environment variable for future child processes launched by helpers.
///
/// Reason: Java cannot mutate the parent environment, but tasks often need to
/// pass config to later `run(...)` calls.
///
/// Usage: `export("HOSTNAME", "desktop"); run("java", "bootstripper.java", "check");`.
///
/// Pitfall: affects only child processes launched after the call, not the
/// already-running Java shell itself.
void export(String name, String value)
{
	ENV.put(name, value);
}
""";
}

String processHelpers()
{
	return """
/// Build a configured `ProcessBuilder`.
///
/// Reason: centralizes `CWD` and `ENV` application for process helpers.
///
/// Usage: prefer `run(...)`, `result(...)`, `out(...)`, or `code(...)`; use this
/// only when a task needs custom redirects.
///
/// Pitfall: mutating the returned builder does not mutate global helper state.
ProcessBuilder process(String... cmd)
{
	var pb = new ProcessBuilder(cmd).directory(CWD.toFile());
	pb.environment().clear();
	pb.environment().putAll(ENV);
	return pb;
}

/// Run a command, stream its IO, and fail on non-zero exit.
///
/// Reason: this is the Java replacement for a normal shell command line. The
/// shell runs snippets with JShell's local execution engine so inherited child IO
/// reaches mise output live.
///
/// Usage: `run("java", "bootstripper.java", "check");`.
///
/// Pitfall: no shell expansion, globbing, pipes, aliases, or quoting. Pass each
/// argv item separately. Use Java helpers for file work.
void run(String... cmd) throws Exception
{
	var code = process(cmd).inheritIO().start().waitFor();
	if (code != 0)
		throw new RuntimeException("command failed (" + code + "): " + String.join(" ", cmd));
}

/// Run a command and capture merged stdout/stderr.
///
/// Reason: command substitution is sometimes needed, but should be explicit.
///
/// Usage: `var status = result("git", "status", "--short");`.
///
/// Pitfall: does not fail on non-zero exit; inspect `code()` or use `out(...)`.
Result result(String... cmd) throws Exception
{
	var p = process(cmd).redirectErrorStream(true).start();
	var out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
	return new Result(p.waitFor(), out);
}

/// Run a command and return output, failing on non-zero exit.
///
/// Reason: terse command substitution for required commands.
///
/// Usage: `var root = out("git", "rev-parse", "--show-toplevel").strip();`.
///
/// Pitfall: output is buffered until the command exits; use `run(...)` for long
/// commands or progress output.
String out(String... cmd) throws Exception
{
	var r = result(cmd);
	if (r.code() != 0)
		throw new RuntimeException(r.out());
	return r.out();
}

/// Run a command and return only its exit code.
///
/// Reason: direct replacement for shell tests like `cmd >/dev/null || ...`.
///
/// Usage: `if (code("git", "diff", "--quiet") != 0) IO.println("dirty");`.
///
/// Pitfall: output is captured and discarded. Use `result(...)` if diagnostics matter.
int code(String... cmd) throws Exception
{
	return result(cmd).code();
}
""";
}

String fileHelpers()
{
	return """
/// Read a UTF-8-ish text file using Java defaults.
///
/// Reason: common task file reads should not shell out to `cat`.
///
/// Usage: `var text = read("mise.toml");`.
///
/// Pitfall: path resolves against `pwd()`, not necessarily the repo root after `cd(...)`.
String read(String file) throws Exception
{
	return Files.readString(path(file));
}

/// Write text to a file, creating parent directories.
///
/// Reason: common task file writes should not shell out to `mkdir`/redirects.
///
/// Usage: `write("target/out.txt", "ok");`.
///
/// Pitfall: overwrites existing content. Use `Files.writeString(...)` directly
/// when append/open options matter.
void write(String file, String text) throws Exception
{
	var p = path(file);
	var parent = p.getParent();
	if (parent != null)
		Files.createDirectories(parent);
	Files.writeString(p, text);
}

/// Create a directory and missing parents.
///
/// Reason: terse Java replacement for `mkdir -p`.
///
/// Usage: `mkdir("build/classes");`.
///
/// Pitfall: succeeds if the directory already exists; fails if a path component
/// is a normal file.
void mkdir(String dir) throws Exception
{
	Files.createDirectories(path(dir));
}
""";
}

String deleteHelpers()
{
	return """
/// Guard destructive helpers against catastrophic targets.
///
/// Reason: `rm(...)` convenience is useful, but deleting HOME, filesystem roots,
/// or the current task directory is never acceptable for dorkfiles tasks.
///
/// Usage: internal helper used by `rm(...)`, `rmdir(...)`, and `rmTree(...)`.
///
/// Pitfall: this is a safety floor, not a permission model. Review destructive
/// paths anyway.
Path protectDelete(Path p)
{
	p = p.toAbsolutePath().normalize();
	var home = Path.of(System.getProperty("user.home")).toAbsolutePath().normalize();
	if (p.getParent() == null || p.equals(home) || p.equals(CWD))
		throw new IllegalArgumentException("refuse to delete: " + p);
	return p;
}

/// Delete a file or symlink if present.
///
/// Reason: `Files.deleteIfExists(...)` is verbose for one-off cleanup tasks.
///
/// Usage: `rm("target", "old.log");` or `rm("old.log");`.
///
/// Pitfall: refuses normal directories; use `rmdir(...)` for empty directories
/// or `rmTree(...)` for recursive deletion.
void rm(String first, String... more) throws Exception
{
	var p = protectDelete(path(first, more));
	if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(p))
		throw new IllegalArgumentException("is a directory; use rmdir or rmTree: " + p);
	Files.deleteIfExists(p);
}

/// Delete an empty directory if present.
///
/// Reason: common cleanup wants `rmdir` semantics, not recursive deletion.
///
/// Usage: `rmdir("target", "empty");`.
///
/// Pitfall: fails when the directory is not empty. That is intentional; choose
/// `rmTree(...)` explicitly for recursive deletion.
void rmdir(String first, String... more) throws Exception
{
	Files.deleteIfExists(protectDelete(path(first, more)));
}

/// Recursively delete a file tree.
///
/// Reason: some build/cache cleanup truly needs recursive deletion, but it
/// should be explicit and guarded.
///
/// Usage: `rmTree("target");`.
///
/// Pitfall: deletes contents recursively. It refuses HOME, filesystem roots, and
/// current `pwd()`, but callers still own choosing the right path.
void rmTree(String first, String... more) throws Exception
{
	var p = protectDelete(path(first, more));
	if (!Files.exists(p, LinkOption.NOFOLLOW_LINKS))
		return;
	if (Files.isSymbolicLink(p) || !Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS))
	{
		Files.delete(p);
		return;
	}
	try (var stream = Files.walk(p))
	{
		for (var x : stream.sorted(Comparator.reverseOrder()).toList())
			Files.deleteIfExists(x);
	}
}
""";
}

String globHelpers()
{
	return """
/// List direct children matching a glob in `pwd()`.
///
/// Reason: simple file selection should not need shell glob expansion.
///
/// Usage: `for (var file : glob("*.symlinks")) IO.println(file);`.
///
/// Pitfall: this is not recursive and uses Java `DirectoryStream` glob syntax,
/// not Bash globstar rules.
List<Path> glob(String pattern) throws Exception
{
	try (var stream = Files.newDirectoryStream(CWD, pattern))
	{
		var paths = new ArrayList<Path>();
		stream.forEach(paths::add);
		paths.sort(Comparator.naturalOrder());
		return paths;
	}
}
""";
}

String whichHelpers()
{
	return """
/// Find an executable on `PATH`.
///
/// Reason: tasks sometimes need an explicit dependency probe without shelling out
/// to `which`/`command -v`.
///
/// Usage: `if (which("podman") == null) throw new IllegalStateException("podman missing");`.
///
/// Pitfall: returns `null` when not found. On Windows it checks `PATHEXT`; on
/// Unix it checks the exact name only.
String which(String name)
{
	var path = ENV.getOrDefault("PATH", "");
	var extensions = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")
		? ENV.getOrDefault("PATHEXT", ".EXE;.CMD;.BAT").split(Pattern.quote(File.pathSeparator))
		: new String[] { "" };
	for (var dir : path.split(Pattern.quote(File.pathSeparator)))
	{
		for (var extension : extensions)
		{
			var candidate = Path.of(dir, name + extension);
			if (Files.isExecutable(candidate))
				return candidate.toString();
		}
	}
	return null;
}
""";
}