use std::path::Path;
#[cfg(unix)]
use std::borrow::Cow;
use regex::Regex;
use lazy_static::lazy_static;
use color_eyre::Result;
use color_eyre::eyre::{self, WrapErr};
use log;
#[cfg(target_os = "linux")]
pub const MAX_SHEBANG_LENGTH_LINUX: usize = 127;
#[derive(Debug, PartialEq)]
pub struct ShebangInfo {
pub interpreter_path: String,
pub interpreter_basename: String,
pub remaining_params: String,
}
lazy_static! {
pub static ref SHEBANG_REGEX: Regex = Regex::new(r"^(#!(?:[ ]*)(/(?:\\ |[^ \n\r\t])*)(.*))$").unwrap();
pub static ref PYTHON_REGEX: Regex = Regex::new(r"^python(?:\d+(?:\.\d+)?)?$").unwrap();
}
#[cfg(unix)]
pub fn is_valid_shebang_length(_shebang: &str) -> bool {
#[cfg(target_os = "linux")]
{
_shebang.len() <= MAX_SHEBANG_LENGTH_LINUX
}
#[cfg(target_os = "macos")]
{
_shebang.len() <= 512
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
true
}
}
#[cfg(unix)]
pub fn convert_shebang_to_env(shebang: Cow<'_, str>) -> Cow<'_, str> {
if let Some(captures) = SHEBANG_REGEX.captures(&shebang) {
let path = captures.get(2).map(|m| m.as_str()).unwrap_or("");
let exe_name = path.rsplit_once('/').map_or(path, |(_, f)| f);
let rest = captures.get(3).map(|m| m.as_str()).unwrap_or("");
if PYTHON_REGEX.is_match(exe_name) {
Cow::Owned(format!(
"#!/bin/sh\n'''exec' \"{}\"{}\" \"$0\" \"$@\" #'''",
path, rest
))
} else {
Cow::Owned(format!("#!/usr/bin/env {}{}", exe_name, rest))
}
} else {
shebang
}
}
fn parse_shebang_line(first_line: &str) -> Result<(String, String)> {
if !first_line.starts_with("#!") {
return Err(eyre::eyre!("No shebang line found"));
}
let interpreter_with_params = first_line[2..].trim().replace("\t", " ");
let (interpreter_path, params) = match interpreter_with_params.split_once(' ') {
Some((path, params)) => (path.to_string(), params.to_string()),
None => (interpreter_with_params.to_string(), String::new()),
};
log::debug!("interpreter_path: '{}', params: '{}'", interpreter_path, params);
Ok((interpreter_path, params))
}
pub fn parse_shebang_for_wrapper(first_line: &str) -> Result<ShebangInfo> {
let (interpreter_path, params) = parse_shebang_line(first_line)
.with_context(|| format!("Failed to parse shebang line: '{}'", first_line))?;
if interpreter_path == "/usr/bin/env" {
if params.is_empty() {
return Err(eyre::eyre!("env requires an interpreter to be specified"));
}
if !params.trim().is_empty() {
let mut param_parts: Vec<&str> = params.split_whitespace().collect();
if param_parts.len() >= 2 && param_parts[0] == "-S" {
param_parts.remove(0);
}
if param_parts.is_empty() {
return Err(eyre::eyre!("env -S requires an interpreter to be specified"));
}
let actual_interpreter = param_parts[0];
let remaining_params = param_parts[1..].join(" ");
return Ok(ShebangInfo {
interpreter_path: format!("/usr/bin/{}", actual_interpreter),
interpreter_basename: actual_interpreter.to_string(),
remaining_params,
});
}
}
if interpreter_path.is_empty() {
return Ok(ShebangInfo {
interpreter_path: String::new(),
interpreter_basename: String::new(),
remaining_params: params,
});
}
let interpreter_basename = Path::new(&interpreter_path).file_name()
.ok_or_else(|| eyre::eyre!("Failed to get interpreter basename from: {}", interpreter_path))?
.to_string_lossy()
.to_string();
Ok(ShebangInfo {
interpreter_path,
interpreter_basename,
remaining_params: params,
})
}
#[cfg(target_os = "linux")]
pub fn strip_shebang(script: &str) -> &str {
let mut lines = script.lines();
if let Some(first_line) = lines.next() {
if first_line.trim_start().starts_with("#!") {
let offset = first_line.len() + match script[first_line.len()..].chars().next() {
Some('\n') => 1,
Some('\r') => if script[first_line.len()..].starts_with("\r\n") { 2 } else { 1 },
_ => 0,
};
&script[offset..]
} else {
script
}
} else {
script
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_based_shebangs() {
let info = parse_shebang_for_wrapper("#!/usr/bin/env python").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python");
assert_eq!(info.interpreter_basename, "python");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3 ").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3.11").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3.11");
assert_eq!(info.interpreter_basename, "python3.11");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python -u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python");
assert_eq!(info.interpreter_basename, "python");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3 -u -O ").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u -O");
let info = parse_shebang_for_wrapper("#!/usr/bin/env node").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/node");
assert_eq!(info.interpreter_basename, "node");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env node --experimental-modules").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/node");
assert_eq!(info.interpreter_basename, "node");
assert_eq!(info.remaining_params, "--experimental-modules");
let info = parse_shebang_for_wrapper("#!/usr/bin/env ruby").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/ruby");
assert_eq!(info.interpreter_basename, "ruby");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env perl").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/perl");
assert_eq!(info.interpreter_basename, "perl");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env php").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/php");
assert_eq!(info.interpreter_basename, "php");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env bash").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env zsh").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/zsh");
assert_eq!(info.interpreter_basename, "zsh");
assert_eq!(info.remaining_params, "");
}
#[test]
fn test_direct_interpreter_shebangs() {
let info = parse_shebang_for_wrapper("#! /bin/sh").unwrap();
assert_eq!(info.interpreter_path, "/bin/sh");
assert_eq!(info.interpreter_basename, "sh");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/bin/bash ").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/bin/bash -e ").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "-e");
let info = parse_shebang_for_wrapper("#!/bin/bash -eu -o pipefail").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "-eu -o pipefail");
let info = parse_shebang_for_wrapper("#!/usr/bin/python3").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/python3.11 -u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3.11");
assert_eq!(info.interpreter_basename, "python3.11");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#!/usr/bin/perl").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/perl");
assert_eq!(info.interpreter_basename, "perl");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/ruby").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/ruby");
assert_eq!(info.interpreter_basename, "ruby");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/node").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/node");
assert_eq!(info.interpreter_basename, "node");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/lua").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/lua");
assert_eq!(info.interpreter_basename, "lua");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/awk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/awk");
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/gawk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/gawk");
assert_eq!(info.interpreter_basename, "gawk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/tclsh").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/tclsh");
assert_eq!(info.interpreter_basename, "tclsh");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/fish").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/fish");
assert_eq!(info.interpreter_basename, "fish");
assert_eq!(info.remaining_params, "");
}
#[test]
fn test_exotic_shebangs() {
let info = parse_shebang_for_wrapper("#!/bin/env python").unwrap();
assert_eq!(info.interpreter_path, "/bin/env");
assert_eq!(info.interpreter_basename, "env");
assert_eq!(info.remaining_params, "python");
let info = parse_shebang_for_wrapper("#!/opt/python/bin/python").unwrap();
assert_eq!(info.interpreter_path, "/opt/python/bin/python");
assert_eq!(info.interpreter_basename, "python");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/local/bin/python3").unwrap();
assert_eq!(info.interpreter_path, "/usr/local/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/home/user/.local/bin/custom-script").unwrap();
assert_eq!(info.interpreter_path, "/home/user/.local/bin/custom-script");
assert_eq!(info.interpreter_basename, "custom-script");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python-config").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python-config");
assert_eq!(info.interpreter_basename, "python-config");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3.11-config").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3.11-config");
assert_eq!(info.interpreter_basename, "python3.11-config");
assert_eq!(info.remaining_params, "");
}
#[test]
fn test_edge_cases() {
let result = parse_shebang_for_wrapper("#!/usr/bin/env ");
assert!(result.is_err());
let result = parse_shebang_for_wrapper("#!/usr/bin/env");
assert!(result.is_err());
let info = parse_shebang_for_wrapper("#! /usr/bin/env python3 -u -O").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u -O");
let info = parse_shebang_for_wrapper("#!/usr/bin/env\tpython3\t-u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#! /bin/bash").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#! /bin/bash -e").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "-e");
let info = parse_shebang_for_wrapper("#! /usr/bin/env python3").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#! /usr/bin/env python3 -u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#! /bin/bash").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!\t/bin/bash").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
}
#[test]
fn test_invalid_shebangs() {
let result = parse_shebang_for_wrapper("python script");
assert!(result.is_err());
let result = parse_shebang_for_wrapper("#python");
assert!(result.is_err());
let result = parse_shebang_for_wrapper("");
assert!(result.is_err());
let result = parse_shebang_for_wrapper("#!");
assert!(result.is_ok());
}
#[test]
fn test_real_world_examples() {
let info = parse_shebang_for_wrapper("#!/usr/bin/env python").unwrap();
assert_eq!(info.interpreter_basename, "python");
let info = parse_shebang_for_wrapper("#!/usr/bin/env node").unwrap();
assert_eq!(info.interpreter_basename, "node");
let info = parse_shebang_for_wrapper("#!/bin/bash").unwrap();
assert_eq!(info.interpreter_basename, "bash");
let info = parse_shebang_for_wrapper("#!/bin/sh").unwrap();
assert_eq!(info.interpreter_basename, "sh");
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3").unwrap();
assert_eq!(info.interpreter_basename, "python3");
let info = parse_shebang_for_wrapper("#!/usr/bin/env ruby").unwrap();
assert_eq!(info.interpreter_basename, "ruby");
let info = parse_shebang_for_wrapper("#!/usr/bin/perl -w").unwrap();
assert_eq!(info.interpreter_basename, "perl");
assert_eq!(info.remaining_params, "-w");
let info = parse_shebang_for_wrapper("#!/usr/bin/awk -f").unwrap();
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
}
#[test]
fn test_user_provided_real_world_cases() {
let info = parse_shebang_for_wrapper("#!/usr/bin/env ruby").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/ruby");
assert_eq!(info.interpreter_basename, "ruby");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/bin/sh").unwrap();
assert_eq!(info.interpreter_path, "/bin/sh");
assert_eq!(info.interpreter_basename, "sh");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/usr/bin/awk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/awk");
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S awk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/awk");
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/env bash").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#!/bin/bash").unwrap();
assert_eq!(info.interpreter_path, "/bin/bash");
assert_eq!(info.interpreter_basename, "bash");
assert_eq!(info.remaining_params, "");
}
#[test]
fn test_env_s_flag_variations() {
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S awk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/awk");
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S python3 -u -O").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u -O");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S python3").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
let info = parse_shebang_for_wrapper("#! /usr/bin/env -S awk -f").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/awk");
assert_eq!(info.interpreter_basename, "awk");
assert_eq!(info.remaining_params, "-f");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S python3 -u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S python3.11 -u").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3.11");
assert_eq!(info.interpreter_basename, "python3.11");
assert_eq!(info.remaining_params, "-u");
let info = parse_shebang_for_wrapper("#!/usr/bin/env -S python-config --version").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python-config");
assert_eq!(info.interpreter_basename, "python-config");
assert_eq!(info.remaining_params, "--version");
}
#[test]
fn test_env_s_flag_edge_cases() {
let info = parse_shebang_for_wrapper("#!/usr/bin/env python3").unwrap();
assert_eq!(info.interpreter_path, "/usr/bin/python3");
assert_eq!(info.interpreter_basename, "python3");
assert_eq!(info.remaining_params, "");
}
#[test]
#[cfg(target_os = "linux")]
fn test_strip_shebang() {
let script = "#!/usr/bin/env lua\nprint('Hello')";
let stripped = strip_shebang(script);
assert_eq!(stripped, "print('Hello')");
let script2 = "#! /usr/bin/lua\nprint('Hello')";
let stripped2 = strip_shebang(script2);
assert_eq!(stripped2, "print('Hello')");
let script3 = "#!/usr/bin/env lua\r\nprint('Hello')";
let stripped3 = strip_shebang(script3);
assert_eq!(stripped3, "print('Hello')");
let script4 = "print('Hello')";
let stripped4 = strip_shebang(script4);
assert_eq!(stripped4, script4);
let script5 = "#!/usr/bin/lua";
let stripped5 = strip_shebang(script5);
assert_eq!(stripped5, "");
let script6 = "";
let stripped6 = strip_shebang(script6);
assert_eq!(stripped6, "");
}
}