#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShimTarget {
pub interpreter: String,
pub prefix_args: Vec<String>,
}
pub fn parse_cmd_shim(contents: &str, shim_dir: &str) -> Option<ShimTarget> {
let dir = shim_dir.trim_end_matches(['\\', '/']);
let expand = |s: &str| s.replace("%~dp0", dir).replace("%dp0%", dir);
let exec = contents.lines().find(|l| l.contains("%*"))?;
let before_star = exec.split("%*").next().unwrap_or("");
if before_star.contains("%_prog%") {
let interpreter = prog_from_set_lines(contents, &expand)?;
let after = before_star.split("%_prog%").nth(1)?.trim_start();
let middle = after.strip_prefix('"').unwrap_or(after);
let prefix_args: Vec<String> = tokenize(middle).into_iter().map(|t| expand(&t)).collect();
if prefix_args.is_empty() && !interpreter.to_ascii_lowercase().ends_with(".exe") {
return None;
}
return Some(ShimTarget {
interpreter,
prefix_args,
});
}
let trimmed = before_star.trim_start();
let inline = trimmed.strip_prefix('@').unwrap_or(trimmed);
let mut tokens = tokenize(inline).into_iter().map(|t| expand(&t));
let interpreter = tokens.next()?;
Some(ShimTarget {
interpreter,
prefix_args: tokens.collect(),
})
}
fn prog_from_set_lines(contents: &str, expand: &impl Fn(&str) -> String) -> Option<String> {
let mut bare: Option<String> = None;
let mut colocated: Option<String> = None;
for line in contents.lines() {
if let Some(val) = parse_set_prog(line) {
if val.contains("%dp0%") || val.contains("%~dp0") {
colocated.get_or_insert_with(|| expand(&val));
} else {
bare.get_or_insert(val);
}
}
}
bare.or(colocated)
}
fn parse_set_prog(line: &str) -> Option<String> {
let trimmed = line.trim().strip_prefix('@').unwrap_or(line.trim());
if !trimmed.to_ascii_lowercase().starts_with("set ") {
return None;
}
let start = trimmed.find("\"_prog=")? + "\"_prog=".len();
let rest = &trimmed[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn tokenize(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_quotes = false;
let mut has_token = false;
for ch in s.chars() {
match ch {
'"' => {
in_quotes = !in_quotes;
has_token = true;
}
c if c.is_whitespace() && !in_quotes => {
if has_token {
out.push(std::mem::take(&mut cur));
has_token = false;
}
}
c => {
cur.push(c);
has_token = true;
}
}
}
if has_token {
out.push(cur);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const NEWER: &str = r#"@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@anthropic-ai\claude-code\cli.js" %*
"#;
const OLDER: &str = r#"@IF EXIST "%~dp0\node.exe" (
@SET "_prog=%~dp0\node.exe"
) ELSE (
@SET "_prog=node"
@SET PATHEXT=%PATHEXT:;.JS;=;%
)
"%_prog%" "%~dp0\..\@anthropic-ai\claude-code\cli.js" %*
"#;
const EXE: &str = r#"@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
"%dp0%\node_modules\@anthropic-ai\claude-code\bin\claude.exe" %*
"#;
#[test]
fn parses_newer_layout_to_node_and_expanded_script() {
let t = parse_cmd_shim(NEWER, r"C:\npm").unwrap();
assert_eq!(t.interpreter, "node");
assert_eq!(
t.prefix_args,
vec![r"C:\npm\node_modules\@anthropic-ai\claude-code\cli.js"]
);
}
#[test]
fn parses_older_dp0_layout() {
let t = parse_cmd_shim(OLDER, r"C:\npm").unwrap();
assert_eq!(t.interpreter, "node");
assert_eq!(
t.prefix_args,
vec![r"C:\npm\..\@anthropic-ai\claude-code\cli.js"]
);
}
#[test]
fn trailing_separator_on_shim_dir_is_normalized() {
let t = parse_cmd_shim(NEWER, r"C:\npm\").unwrap();
assert_eq!(
t.prefix_args,
vec![r"C:\npm\node_modules\@anthropic-ai\claude-code\cli.js"]
);
}
#[test]
fn keeps_interpreter_flags_before_the_script() {
let shim = "SET \"_prog=node\"\n\"%_prog%\" --no-warnings \"%dp0%\\cli.js\" %*\n";
let t = parse_cmd_shim(shim, r"C:\x").unwrap();
assert_eq!(t.interpreter, "node");
assert_eq!(t.prefix_args, vec!["--no-warnings", r"C:\x\cli.js"]);
}
#[test]
fn falls_back_to_colocated_interpreter_when_no_bare_name() {
let shim = "SET \"_prog=%dp0%\\node.exe\"\n\"%_prog%\" \"%dp0%\\cli.js\" %*\n";
let t = parse_cmd_shim(shim, r"C:\x").unwrap();
assert_eq!(t.interpreter, r"C:\x\node.exe");
assert_eq!(t.prefix_args, vec![r"C:\x\cli.js"]);
}
#[test]
fn parses_native_exe_layout_with_empty_prefix() {
let t = parse_cmd_shim(EXE, r"C:\npm").unwrap();
assert_eq!(
t.interpreter,
r"C:\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe"
);
assert!(t.prefix_args.is_empty(), "{:?}", t.prefix_args);
}
#[test]
fn parses_inline_exe_with_leading_at_and_no_find_dp0() {
let shim = "@\"%~dp0\\node_modules\\pkg\\tool.exe\" %*\r\n";
let t = parse_cmd_shim(shim, r"C:\g").unwrap();
assert_eq!(t.interpreter, r"C:\g\node_modules\pkg\tool.exe");
assert!(t.prefix_args.is_empty());
}
#[test]
fn non_shim_text_returns_none() {
assert!(parse_cmd_shim("echo hello\nexit /b 0\n", r"C:\x").is_none());
}
#[test]
fn missing_exec_line_returns_none() {
assert!(parse_cmd_shim("SET \"_prog=node\"\n", r"C:\x").is_none());
}
#[test]
fn node_prog_without_a_script_returns_none() {
let shim = "SET \"_prog=node\"\n\"%_prog%\" %*\n";
assert!(parse_cmd_shim(shim, r"C:\x").is_none());
}
#[test]
fn parse_set_prog_ignores_unrelated_lines() {
assert_eq!(parse_set_prog("SET PATHEXT=%PATHEXT:;.JS;=;%"), None);
assert_eq!(parse_set_prog(" not a set line"), None);
assert_eq!(
parse_set_prog("@SET \"_prog=node\""),
Some("node".to_string())
);
}
#[test]
fn tokenize_handles_quotes_and_whitespace() {
assert_eq!(tokenize(" \"a b\" c "), vec!["a b", "c"]);
assert_eq!(tokenize(""), Vec::<String>::new());
assert_eq!(tokenize("\"\""), vec![""]);
}
}