pub mod argument;
pub mod program;
pub mod variable;
pub mod variable_name;
pub mod variable_value;
pub mod working_directory;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::process::Stdio;
use std::time::Instant;
use tokio::process::Command;
pub use self::argument::Argument;
pub use self::program::Program;
pub use self::variable::Variable;
pub use self::variable_name::VariableName;
pub use self::variable_value::VariableValue;
pub use self::working_directory::WorkingDirectory;
use crate::error::RunCommandError;
use crate::execution::Capture;
use crate::execution::Execution;
use crate::process_id::ProcessId;
use crate::run::Run;
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct Invocation {
program: Program,
arguments: Vec<Argument>,
environment: Vec<Variable>,
removals: Vec<VariableName>,
working_directory: Option<WorkingDirectory>,
}
impl Invocation {
pub fn new(program: impl Into<Program>) -> Self {
Self {
program: program.into(),
arguments: Vec::new(),
environment: Vec::new(),
removals: Vec::new(),
working_directory: None,
}
}
pub fn arg(mut self, argument: impl Into<Argument>) -> Self {
self.arguments.push(argument.into());
self
}
pub fn args(mut self, arguments: impl IntoIterator<Item = impl Into<Argument>>) -> Self {
self.arguments.extend(arguments.into_iter().map(Into::into));
self
}
pub fn arguments(&self) -> &[Argument] {
&self.arguments
}
fn command(&self) -> Command {
let mut command = Command::new(self.program.get());
command
.args(self.arguments.iter().map(Argument::get))
.envs(
self.environment
.iter()
.map(|variable| (variable.name().get(), variable.value().get())),
)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
for name in &self.removals {
command.env_remove(name.get());
}
if let Some(directory) = &self.working_directory {
command.current_dir(directory.get());
}
command
}
pub fn env(mut self, name: impl Into<VariableName>, value: impl Into<VariableValue>) -> Self {
self.set(Variable::new(name, value));
self
}
pub fn environment(&self) -> &[Variable] {
&self.environment
}
pub fn env_remove(mut self, name: impl Into<VariableName>) -> Self {
let name = name.into();
self.environment.retain(|variable| *variable.name() != name);
if !self.removals.contains(&name) {
self.removals.push(name);
}
self
}
pub fn removals(&self) -> &[VariableName] {
&self.removals
}
pub fn envs(mut self, variables: impl IntoIterator<Item = impl Into<Variable>>) -> Self {
for variable in variables {
self.set(variable.into());
}
self
}
pub fn in_directory(mut self, directory: impl Into<WorkingDirectory>) -> Self {
self.working_directory = Some(directory.into());
self
}
pub fn program(&self) -> &Program {
&self.program
}
pub async fn run(&self) -> Result<Execution, RunCommandError> {
let start = Instant::now();
let child =
self.command()
.spawn()
.map_err(|source| RunCommandError::UnstartableCommand {
invocation: Box::new(self.clone()),
source,
})?;
let id = child.id().map(ProcessId::from);
let output =
child
.wait_with_output()
.await
.map_err(|source| RunCommandError::IncompleteRun {
invocation: Box::new(self.clone()),
source,
})?;
Ok(Execution::new(
self.clone(),
id,
output.status,
Capture::new(output.stdout),
Capture::new(output.stderr),
start.elapsed(),
))
}
fn set(&mut self, variable: Variable) {
self.removals.retain(|name| name != variable.name());
let existing = self
.environment
.iter_mut()
.find(|existing| existing.name() == variable.name());
match existing {
Some(existing) => *existing = variable,
None => self.environment.push(variable),
}
}
pub fn start(&self) -> Result<Run, RunCommandError> {
let started = Instant::now();
let child =
self.command()
.spawn()
.map_err(|source| RunCommandError::UnstartableCommand {
invocation: Box::new(self.clone()),
source,
})?;
Ok(Run::new(self.clone(), child, started))
}
pub fn working_directory(&self) -> Option<&WorkingDirectory> {
self.working_directory.as_ref()
}
}
impl Display for Invocation {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
for name in &self.removals {
write!(formatter, "-{name} ")?;
}
for variable in &self.environment {
write!(formatter, "{}=", variable.name())?;
write_word(formatter, &variable.value().get().to_string_lossy())?;
formatter.write_str(" ")?;
}
write_word(formatter, &self.program.get().to_string_lossy())?;
for argument in &self.arguments {
formatter.write_str(" ")?;
write_word(formatter, &argument.get().to_string_lossy())?;
}
Ok(())
}
}
fn write_word(formatter: &mut Formatter<'_>, word: &str) -> FmtResult {
if word.is_empty() || word.contains(char::is_whitespace) {
write!(formatter, "\"{word}\"")
} else {
formatter.write_str(word)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use super::*;
#[test]
fn invocation_is_send_and_sync() {
fn assert_send_and_sync<T: Send + Sync>() {}
assert_send_and_sync::<Invocation>();
}
}