repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
scripts/mise_shell.java
Rawimport 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;
}
""";
}