use clap::Subcommand;
use std::path::{Path, PathBuf};
mod bootstrap;
mod config;
mod devcontainer;
mod git_pre_commit;
mod github_action;
mod task_docs;
mod task_stubs;
mod tool_stub;
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "gen", alias = "g")]
pub struct Generate {
#[clap(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Bootstrap(bootstrap::Bootstrap),
Config(config::Config),
Devcontainer(devcontainer::Devcontainer),
GitPreCommit(git_pre_commit::GitPreCommit),
GithubAction(github_action::GithubAction),
TaskDocs(task_docs::TaskDocs),
TaskStubs(task_stubs::TaskStubs),
ToolStub(tool_stub::ToolStub),
}
impl Commands {
pub async fn run(self) -> eyre::Result<()> {
match self {
Self::Bootstrap(cmd) => cmd.run().await,
Self::Config(cmd) => cmd.run().await,
Self::Devcontainer(cmd) => cmd.run().await,
Self::GitPreCommit(cmd) => cmd.run().await,
Self::GithubAction(cmd) => cmd.run().await,
Self::TaskDocs(cmd) => cmd.run().await,
Self::TaskStubs(cmd) => cmd.run().await,
Self::ToolStub(cmd) => cmd.run().await,
}
}
}
impl Generate {
pub async fn run(self) -> eyre::Result<()> {
self.command.run().await
}
}
pub(super) fn windows_launcher_path(stub: &Path) -> Option<PathBuf> {
let name = stub.file_name()?.to_str()?;
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
if matches!(ext.as_deref(), Some("cmd" | "bat" | "exe")) {
return None;
}
Some(stub.with_file_name(format!("{name}.cmd")))
}
pub(super) const WINDOWS_LAUNCHER_MARKER: &str = "rem generated by mise";
pub(super) fn windows_launcher_body(command: &str) -> String {
format!("@echo off\r\n{WINDOWS_LAUNCHER_MARKER}\r\n{command} %*\r\n")
}
pub(super) fn is_generated_launcher(contents: &str) -> bool {
let mut lines = contents.lines();
matches!(lines.next(), Some("@echo off"))
&& lines.next() == Some(WINDOWS_LAUNCHER_MARKER)
&& lines.next().is_some_and(|line| line.ends_with(" %*"))
&& lines.next().is_none()
}
pub(super) fn cmd_quote(s: &str) -> String {
format!("\"{}\"", s.replace('%', "%%"))
}
#[cfg(test)]
mod windows_launcher_tests {
use super::*;
#[test]
fn launcher_sits_beside_the_stub() {
assert_eq!(
windows_launcher_path(Path::new("bin/hello")),
Some(PathBuf::from("bin/hello.cmd"))
);
assert_eq!(
windows_launcher_path(Path::new("my.tool")),
Some(PathBuf::from("my.tool.cmd"))
);
}
#[test]
fn a_name_that_is_already_executable_gets_none() {
for name in ["mytool.cmd", "mytool.BAT", "mytool.exe"] {
assert_eq!(windows_launcher_path(Path::new(name)), None, "{name}");
}
}
#[test]
fn body_forwards_arguments() {
assert_eq!(
windows_launcher_body("mise run hello"),
"@echo off\r\nrem generated by mise\r\nmise run hello %*\r\n"
);
}
#[test]
fn every_body_carries_the_ownership_marker() {
for command in ["mise run hello", r#"mise tool-stub "%~dpn0""#] {
let body = windows_launcher_body(command);
assert!(
body.lines().nth(1) == Some(WINDOWS_LAUNCHER_MARKER),
"{body:?}"
);
}
}
#[test]
fn a_launcher_is_recognised_whatever_command_it_carries() {
for command in [
"mise run hello",
r#""C:\Program Files\mise.exe" run hello"#,
r#"mise tool-stub "%~dpn0""#,
"some-other-binary run build",
] {
assert!(
is_generated_launcher(&windows_launcher_body(command)),
"{command}"
);
}
}
#[test]
fn a_launcher_without_the_marker_is_not_ours() {
for contents in [
"@echo off\r\nmise run hello %*\r\n",
"@echo off\r\nrem hand written\r\nmise run hello %*\r\n",
"rem generated by mise\r\nmise run hello %*\r\n",
"@echo off\r\nrem generated by mise\r\nmise run hello %*\r\necho done\r\n",
"@echo off\r\nrem generated by mise\r\nmise run hello\r\n",
"",
] {
assert!(!is_generated_launcher(contents), "{contents:?}");
}
}
#[test]
fn quoting_survives_cmd_metacharacters() {
assert_eq!(cmd_quote("mise"), "\"mise\"");
assert_eq!(
cmd_quote(r"C:\Program Files\mise.exe"),
"\"C:\\Program Files\\mise.exe\""
);
assert_eq!(cmd_quote(r"C:\a&b\mise.exe"), "\"C:\\a&b\\mise.exe\"");
assert_eq!(cmd_quote(r"C:\p%c\mise.exe"), "\"C:\\p%%c\\mise.exe\"");
assert_eq!(cmd_quote("%PATH%"), "\"%%PATH%%\"");
}
}