use std::{
ffi::OsStr,
ops::{Deref, DerefMut},
path::Path,
};
use anyhow::Context;
use anyhow::bail;
use colored::Colorize;
pub struct Command {
inner: std::process::Command,
value_replace: Box<dyn Fn(&OsStr) -> String>,
}
impl Deref for Command {
type Target = std::process::Command;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Command {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Command {
pub fn new<S>(
program: S,
workdir: &Path,
value_replace: impl Fn(&OsStr) -> String + 'static,
) -> Command
where
S: AsRef<OsStr>,
{
let mut cmd = std::process::Command::new(program);
cmd.current_dir(workdir);
cmd.env("WORKSPACE_FOLDER", workdir.display().to_string());
Self {
inner: cmd,
value_replace: Box::new(value_replace),
}
}
pub fn print_cmd(&self) {
let program = self.get_program().to_string_lossy();
let mut cmd_str = program.into_owned();
for arg in self.get_args() {
cmd_str.push(' ');
cmd_str.push_str(arg.to_string_lossy().as_ref());
}
println!("{}", cmd_str.purple().bold());
}
pub fn into_std(self) -> std::process::Command {
self.inner
}
pub fn run(&mut self) -> anyhow::Result<()> {
self.print_cmd();
let status = self.status()?;
if !status.success() {
bail!("failed with status: {status}");
}
Ok(())
}
pub fn arg<S>(&mut self, arg: S) -> &mut Command
where
S: AsRef<OsStr>,
{
let value = (self.value_replace)(arg.as_ref());
self.inner.arg(value);
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Command
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
for arg in args {
self.arg(arg.as_ref());
}
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Command
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let value = (self.value_replace)(val.as_ref());
self.inner.env(key, value);
self
}
}
pub trait PathResultExt<T> {
fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
where
P: AsRef<Path>;
}
impl<T, E> PathResultExt<T> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
where
P: AsRef<Path>,
{
let path = path.as_ref().to_path_buf();
self.with_context(move || format!("{action}: {}", path.display()))
}
}
pub fn replace_env_placeholders(input: &str) -> anyhow::Result<String> {
replace_placeholders(input, |placeholder| {
if let Some(env_var_name) = placeholder.strip_prefix("env:") {
return Ok(Some(std::env::var(env_var_name).unwrap_or_default()));
}
Ok(None)
})
}
pub fn replace_placeholders<F>(input: &str, mut resolver: F) -> anyhow::Result<String>
where
F: FnMut(&str) -> anyhow::Result<Option<String>>,
{
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '$' && chars.peek() == Some(&'{') {
chars.next(); let mut placeholder = String::new();
let mut brace_count = 1;
let mut found_closing_brace = false;
for ch in chars.by_ref() {
if ch == '{' {
brace_count += 1;
placeholder.push(ch);
} else if ch == '}' {
brace_count -= 1;
if brace_count == 0 {
found_closing_brace = true;
break;
} else {
placeholder.push(ch);
}
} else {
placeholder.push(ch);
}
}
if found_closing_brace {
if let Some(value) = resolver(&placeholder)? {
result.push_str(&value);
} else {
result.push_str("${");
result.push_str(&placeholder);
result.push('}');
}
} else {
result.push_str("${");
result.push_str(&placeholder);
}
} else {
result.push(ch);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_replace_placeholders_supports_custom_variables() {
unsafe {
env::set_var("OSTOOL_TEST_CUSTOM_ENV", "env-value");
}
let result = replace_placeholders(
"workspace=${workspace}, package=${package}, env=${env:OSTOOL_TEST_CUSTOM_ENV}",
|placeholder| {
Ok(match placeholder {
"workspace" => Some("/tmp/workspace".into()),
"package" => Some("/tmp/workspace/kernel".into()),
p if p.starts_with("env:") => Some(env::var(&p[4..]).unwrap_or_default()),
_ => None,
})
},
)
.unwrap();
assert_eq!(
result,
"workspace=/tmp/workspace, package=/tmp/workspace/kernel, env=env-value"
);
}
#[test]
fn test_replace_env_placeholders() {
unsafe {
env::set_var("OSTOOL_TEST_HOME_REPLACE", "/home/test");
env::set_var("OSTOOL_TEST_PATH_REPLACE", "/usr/local/bin");
}
assert_eq!(
replace_env_placeholders("${env:OSTOOL_TEST_HOME_REPLACE}").unwrap(),
"/home/test"
);
assert_eq!(
replace_env_placeholders(
"${env:OSTOOL_TEST_HOME_REPLACE}:${env:OSTOOL_TEST_PATH_REPLACE}"
)
.unwrap(),
"/home/test:/usr/local/bin"
);
let result = replace_env_placeholders("${env:NON_EXISTENT}");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "");
assert_eq!(
replace_env_placeholders("Path: ${env:OSTOOL_TEST_HOME_REPLACE}/bin").unwrap(),
"Path: /home/test/bin"
);
assert_eq!(
replace_env_placeholders("${not_env:placeholder}").unwrap(),
"${not_env:placeholder}"
);
assert_eq!(
replace_env_placeholders("Just a normal string").unwrap(),
"Just a normal string"
);
assert_eq!(replace_env_placeholders("").unwrap(), "");
}
#[test]
fn test_nested_braces() {
unsafe {
env::set_var("OSTOOL_TEST_VAR_NESTED", "value");
}
assert_eq!(
replace_env_placeholders("${env:OSTOOL_TEST_VAR_NESTED} and ${other:placeholder}")
.unwrap(),
"value and ${other:placeholder}"
);
}
#[test]
fn test_replace_placeholders_keeps_unknown_and_legacy_placeholders() {
let result = replace_placeholders(
"${workspaceFolder}:${unknown}:${workspace}",
|placeholder| {
Ok(match placeholder {
"workspaceFolder" => Some("/legacy".into()),
"workspace" => Some("/modern".into()),
_ => None,
})
},
)
.unwrap();
assert_eq!(result, "/legacy:${unknown}:/modern");
}
#[test]
fn test_real_env_vars() {
if let Ok(home) = env::var("HOME") {
assert_eq!(replace_env_placeholders("${env:HOME}").unwrap(), home);
}
}
#[test]
fn test_edge_cases() {
assert_eq!(replace_env_placeholders("${").unwrap(), "${");
assert_eq!(replace_env_placeholders("${env").unwrap(), "${env");
assert_eq!(replace_env_placeholders("${env:").unwrap(), "${env:");
assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
let result = replace_env_placeholders("${env:}");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "");
assert_eq!(replace_env_placeholders("$").unwrap(), "$");
assert_eq!(replace_env_placeholders("$$").unwrap(), "$$");
unsafe {
env::set_var("OSTOOL-TEST-VAR-EDGE", "dash-value");
env::set_var("OSTOOL_TEST_VAR_EDGE", "underscore-value");
}
assert_eq!(
replace_env_placeholders("${env:OSTOOL-TEST-VAR-EDGE}").unwrap(),
"dash-value"
);
assert_eq!(
replace_env_placeholders("${env:OSTOOL_TEST_VAR_EDGE}").unwrap(),
"underscore-value"
);
unsafe {
env::set_var("OSTOOL_EMPTY_VAR_EDGE", "");
}
assert_eq!(
replace_env_placeholders("${env:OSTOOL_EMPTY_VAR_EDGE}").unwrap(),
""
);
}
#[test]
fn test_malformed_placeholders() {
assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
assert_eq!(replace_env_placeholders("${env}").unwrap(), "${env}");
assert_eq!(replace_env_placeholders("${:VAR}").unwrap(), "${:VAR}");
unsafe {
env::set_var("OSTOOL_VAR_MALFORMED", "value");
}
assert_eq!(
replace_env_placeholders("${env:OSTOOL_VAR_MALFORMED}}").unwrap(),
"value}"
);
assert_eq!(replace_env_placeholders("{env:VAR}").unwrap(), "{env:VAR}");
assert_eq!(replace_env_placeholders("$env:VAR}").unwrap(), "$env:VAR}");
}
}