use crate::ast::{Arg, FnCall};
use crate::value::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EnvKind {
Exec,
Shell,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Environment {
pub alias: String,
pub kind: EnvKind,
pub target: String,
}
pub const SCHEMES: &[&str] = &["exec", "shell", "python"];
impl Environment {
pub fn is_environment_target(target: &str) -> bool {
target
.split_once(':')
.is_some_and(|(scheme, _)| SCHEMES.contains(&scheme))
}
pub fn parse_mount(arg: &str) -> Option<crate::Result<Environment>> {
let (alias, target) = arg.split_once('=')?;
if !Self::is_environment_target(target) {
return None;
}
Some(Self::parse(alias, target))
}
pub fn parse(alias: &str, spec: &str) -> crate::Result<Environment> {
let bad = |m: String| crate::QuarbError::parse(m);
if !is_alias(alias) {
return Err(bad(format!(
"an environment's alias is a word ('{alias}' given): write 'ALIAS=SCHEME:TARGET'"
)));
}
let (scheme, target) = spec.split_once(':').unwrap_or((spec, ""));
let (target, _options) = target.split_once('?').unwrap_or((target, ""));
match scheme {
"exec" => {
if target.is_empty() {
return Err(bad(format!(
"'{alias}=exec:' names no target: a directory of programs or one \
program ('{alias}=exec:~/go/bin')"
)));
}
let expanded = expand_home(target);
if !std::path::Path::new(&expanded).exists() {
return Err(bad(format!(
"'{alias}=exec:{target}': no such directory or program"
)));
}
Ok(Environment {
alias: alias.to_string(),
kind: EnvKind::Exec,
target: expanded,
})
}
"shell" => {
if !target.is_empty() {
return Err(bad(format!(
"'{alias}=shell:' takes no target: it is the user's shell ('{target}' given)"
)));
}
Ok(Environment {
alias: alias.to_string(),
kind: EnvKind::Shell,
target: String::new(),
})
}
"python" => Err(crate::QuarbError::unsupported(format!(
"the python: environment ('{alias}=python:{target}'): the native framing \
arrives with the plugin round; exec: and shell: are built"
))),
other => Err(bad(format!("'{other}:' is not an environment kind"))),
}
}
fn resolve(&self, name: &str) -> crate::Result<Program> {
match self.kind {
EnvKind::Exec => {
let path = std::path::Path::new(&self.target);
if path.is_dir() {
let p = path.join(name);
if p.is_file() {
return Ok(Program::Path(p));
}
Err(crate::QuarbError::unsupported(format!(
"environment '{}' has no command '{name}' (exec:{})",
self.alias, self.target
)))
} else if path
.file_name()
.is_some_and(|f| f.to_string_lossy() == name)
|| path
.file_stem()
.is_some_and(|f| f.to_string_lossy() == name)
{
Ok(Program::Path(path.to_path_buf()))
} else {
Err(crate::QuarbError::unsupported(format!(
"environment '{}' has one command, '{}' (exec:{}), not '{name}'",
self.alias,
path.file_stem()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
self.target
)))
}
}
EnvKind::Shell => Ok(Program::ShellWord(name.to_string())),
}
}
pub fn argv(operands: &[String], options: &[(String, Option<String>)]) -> Vec<String> {
let mut words = Vec::new();
for (name, value) in options {
match value {
Some(v) if name.starts_with("--") => words.push(format!("{name}={v}")),
Some(v) => {
words.push(name.clone());
words.push(v.clone());
}
None => words.push(name.clone()),
}
}
words.extend(operands.iter().cloned());
words
}
pub fn call(&self, name: &str, argv: &[String], stdin: &str) -> Result<String, CallFailure> {
let program = self
.resolve(name)
.map_err(|e| CallFailure::Unknown(e.to_string()))?;
let mut command = match &program {
Program::Path(p) => {
let mut c = std::process::Command::new(p);
c.args(argv);
c
}
Program::ShellWord(word) => {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string());
let mut c = std::process::Command::new(shell);
c.arg("-c").arg("\"$0\" \"$@\"").arg(word).args(argv);
c
}
};
let mut child = command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.map_err(|e| {
CallFailure::Unknown(format!(
"environment '{}': cannot run '{name}': {e}",
self.alias
))
})?;
if let Some(pipe) = child.stdin.take() {
let mut text = stdin.to_string();
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
feed_stdin(pipe, text);
}
let out = child.wait_with_output().map_err(|e| {
CallFailure::Failed(format!(
"environment '{}': '{name}' did not finish: {e}",
self.alias
))
})?;
if out.status.code() == Some(127) && matches!(program, Program::ShellWord(_)) {
return Err(CallFailure::Unknown(format!(
"environment '{}' has no command '{name}' (shell: command not found)",
self.alias
)));
}
if !out.status.success() {
return Err(CallFailure::Failed(format!(
"environment '{}': '{name}' failed ({})",
self.alias, out.status
)));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
}
#[derive(Debug, Clone)]
pub enum CallFailure {
Unknown(String),
Failed(String),
}
impl CallFailure {
pub fn message(&self) -> &str {
match self {
CallFailure::Unknown(m) | CallFailure::Failed(m) => m,
}
}
}
pub(crate) fn feed_stdin(mut pipe: std::process::ChildStdin, text: String) {
use std::io::Write;
std::thread::spawn(move || {
#[cfg(unix)]
unsafe {
let mut set: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut set);
libc::sigaddset(&mut set, libc::SIGPIPE);
libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut());
}
let _ = pipe.write_all(text.as_bytes());
});
}
enum Program {
Path(std::path::PathBuf),
ShellWord(String),
}
fn is_alias(s: &str) -> bool {
let mut chars = s.chars();
chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}
fn expand_home(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/")
&& let Ok(home) = std::env::var("HOME")
{
return format!("{home}/{rest}");
}
path.to_string()
}
pub fn split_env(name: &str) -> Option<(&str, &str)> {
let (alias, cmd) = name.split_once(':')?;
(!alias.is_empty() && !cmd.is_empty()).then_some((alias, cmd))
}
pub fn literal_options(call: &FnCall) -> Vec<(String, Option<String>)> {
call.args
.iter()
.filter_map(|a| match a {
Arg::Opt { name, value } => Some((
name.clone(),
value.as_ref().map(|v| match v {
crate::ast::Operand::Lit(l) => l.to_string(),
other => crate::unparse::operand_text(other),
}),
)),
_ => None,
})
.collect()
}
pub fn line_of(v: &Value) -> String {
match v {
Value::Record(_) | Value::List(_) => v.to_json(),
other => other.to_string(),
}
}