use std::ffi::{OsStr, OsString};
use std::fmt;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use encoding_rs::{Encoding, UTF_8};
use crate::buffer::OutputBufferPolicy;
use crate::error::Result;
use crate::pump::LineHandler;
use crate::result::ProcessResult;
use crate::runner::JobRunner;
use crate::running::RunningProcess;
use crate::stdin::Stdin;
#[derive(Clone)]
pub struct Command {
program: OsString,
args: Vec<OsString>,
cwd: Option<OsString>,
envs: Vec<(OsString, Option<OsString>)>,
env_clear: bool,
stdin: Option<Stdin>,
keep_stdin_open: bool,
timeout: Option<Duration>,
stdout_handler: Option<LineHandler>,
stderr_handler: Option<LineHandler>,
output_buffer: OutputBufferPolicy,
stdout_encoding: &'static Encoding,
stderr_encoding: &'static Encoding,
}
impl Command {
pub fn new(program: impl AsRef<OsStr>) -> Self {
Self {
program: program.as_ref().to_os_string(),
args: Vec::new(),
cwd: None,
envs: Vec::new(),
env_clear: false,
stdin: None,
keep_stdin_open: false,
timeout: None,
stdout_handler: None,
stderr_handler: None,
output_buffer: OutputBufferPolicy::unbounded(),
stdout_encoding: UTF_8,
stderr_encoding: UTF_8,
}
}
pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Self {
self.args.push(arg.as_ref().to_os_string());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.args
.extend(args.into_iter().map(|a| a.as_ref().to_os_string()));
self
}
pub fn current_dir(mut self, dir: impl AsRef<Path>) -> Self {
self.cwd = Some(dir.as_ref().as_os_str().to_os_string());
self
}
pub fn env(mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
self.envs.push((
key.as_ref().to_os_string(),
Some(value.as_ref().to_os_string()),
));
self
}
pub fn env_remove(mut self, key: impl AsRef<OsStr>) -> Self {
self.envs.push((key.as_ref().to_os_string(), None));
self
}
pub fn env_clear(mut self) -> Self {
self.env_clear = true;
self
}
pub fn stdin(mut self, stdin: Stdin) -> Self {
self.stdin = Some(stdin);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn keep_stdin_open(mut self) -> Self {
self.keep_stdin_open = true;
self
}
pub fn on_stdout_line<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.stdout_handler = Some(Arc::new(handler));
self
}
pub fn on_stderr_line<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.stderr_handler = Some(Arc::new(handler));
self
}
pub fn output_buffer(mut self, policy: OutputBufferPolicy) -> Self {
self.output_buffer = policy;
self
}
pub fn stdout_encoding(mut self, encoding: &'static Encoding) -> Self {
self.stdout_encoding = encoding;
self
}
pub fn stderr_encoding(mut self, encoding: &'static Encoding) -> Self {
self.stderr_encoding = encoding;
self
}
pub fn encoding(mut self, encoding: &'static Encoding) -> Self {
self.stdout_encoding = encoding;
self.stderr_encoding = encoding;
self
}
pub(crate) fn keeps_stdin_open(&self) -> bool {
self.keep_stdin_open
}
pub(crate) fn stdout_handler(&self) -> Option<LineHandler> {
self.stdout_handler.clone()
}
pub(crate) fn stderr_handler(&self) -> Option<LineHandler> {
self.stderr_handler.clone()
}
pub(crate) fn output_buffer_policy(&self) -> OutputBufferPolicy {
self.output_buffer
}
pub(crate) fn out_encoding(&self) -> &'static Encoding {
self.stdout_encoding
}
pub(crate) fn err_encoding(&self) -> &'static Encoding {
self.stderr_encoding
}
pub(crate) fn program_name(&self) -> String {
self.program.to_string_lossy().into_owned()
}
pub fn program(&self) -> &OsStr {
&self.program
}
pub fn arguments(&self) -> &[OsString] {
&self.args
}
pub fn working_dir(&self) -> Option<&Path> {
self.cwd.as_deref().map(Path::new)
}
pub fn env_overrides(&self) -> &[(OsString, Option<OsString>)] {
&self.envs
}
pub fn stdin_source(&self) -> Option<&Stdin> {
self.stdin.as_ref()
}
pub fn configured_timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn to_tokio_command(&self) -> tokio::process::Command {
self.build_tokio()
}
pub(crate) fn build_tokio(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new(&self.program);
cmd.args(&self.args);
if let Some(cwd) = &self.cwd {
cmd.current_dir(cwd);
}
if self.env_clear {
cmd.env_clear();
}
for (key, value) in &self.envs {
match value {
Some(val) => {
cmd.env(key, val);
}
None => {
cmd.env_remove(key);
}
}
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
if self.keep_stdin_open {
cmd.stdin(Stdio::piped());
} else {
match &self.stdin {
Some(src) => {
cmd.stdin(src.stdio());
}
None => {
cmd.stdin(Stdio::null());
}
}
}
cmd
}
pub async fn start(&self) -> Result<RunningProcess> {
JobRunner::new().start(self).await
}
pub async fn output_string(&self) -> Result<ProcessResult<String>> {
JobRunner::new().start(self).await?.output_string().await
}
pub async fn output_bytes(&self) -> Result<ProcessResult<Vec<u8>>> {
JobRunner::new().start(self).await?.output_bytes().await
}
pub async fn exit_code(&self) -> Result<i32> {
JobRunner::new().start(self).await?.wait().await
}
pub async fn run(&self) -> Result<String> {
let result = self.output_string().await?.ensure_success()?;
Ok(result.into_stdout().trim_end().to_owned())
}
pub async fn first_line<F>(&self, predicate: F) -> Result<Option<String>>
where
F: Fn(&str) -> bool,
{
use tokio_stream::StreamExt;
let mut process = JobRunner::new().start(self).await?;
let mut lines = process.stdout_lines();
while let Some(line) = lines.next().await {
if predicate(&line) {
return Ok(Some(line));
}
}
Ok(None)
}
}
impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Command")
.field("program", &self.program)
.field("args", &self.args)
.field("cwd", &self.cwd)
.field("envs", &self.envs)
.field("env_clear", &self.env_clear)
.field("stdin", &self.stdin)
.field("keep_stdin_open", &self.keep_stdin_open)
.field("timeout", &self.timeout)
.field("has_stdout_handler", &self.stdout_handler.is_some())
.field("has_stderr_handler", &self.stderr_handler.is_some())
.field("output_buffer", &self.output_buffer)
.field("stdout_encoding", &self.stdout_encoding.name())
.field("stderr_encoding", &self.stderr_encoding.name())
.finish()
}
}