use std::{
collections::HashMap,
path::{Path, PathBuf},
};
#[derive(thiserror::Error, Debug)]
pub enum CmdError {
#[error("No command name was provided")]
NoCommandNameProvided,
}
use super::Runnable;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cmd {
pub(crate) cmd: String,
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) args: Vec<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) options: CmdOptions,
}
impl Cmd {
pub fn new<S>(cmd: S) -> Self
where
S: Into<String>,
{
Self {
cmd: cmd.into(),
args: Vec::new(),
options: CmdOptions::default(),
}
}
pub fn with_args<S, T, I>(cmd: S, args: I) -> Self
where
S: Into<String>,
T: Into<String>,
I: IntoIterator<Item = T>,
{
Self {
cmd: cmd.into(),
args: args.into_iter().map(Into::into).collect(),
options: CmdOptions::default(),
}
}
pub fn with_options<S>(cmd: S, options: CmdOptions) -> Self
where
S: Into<String>,
{
Self {
cmd: cmd.into(),
args: Vec::new(),
options,
}
}
pub fn with_args_and_options<S, T, I>(cmd: S, args: I, options: CmdOptions) -> Self
where
S: Into<String>,
T: Into<String>,
I: IntoIterator<Item = T>,
{
Self {
cmd: cmd.into(),
args: args.into_iter().map(Into::into).collect(),
options,
}
}
pub fn parse(cmd_string: &str) -> Result<Self, CmdError> {
let mut parts = cmd_string.split_ascii_whitespace();
if let Some(cmd) = parts.next() {
Ok(Cmd::with_args(cmd, parts))
} else {
Err(CmdError::NoCommandNameProvided)
}
}
pub fn parse_with_options(cmd_string: &str, options: CmdOptions) -> Result<Self, CmdError> {
let mut cmd = Self::parse(cmd_string)?;
cmd.options = options;
Ok(cmd)
}
pub fn set_args<S, I>(&mut self, args: I)
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.args = args.into_iter().map(Into::into).collect();
}
pub fn set_options(&mut self, options: CmdOptions) {
self.options = options;
}
pub fn add_arg<S>(&mut self, arg: S)
where
S: Into<String>,
{
self.args.push(arg.into());
}
pub fn cmd(&self) -> &str {
&self.cmd
}
pub fn args(&self) -> &[String] {
&self.args
}
pub fn options(&self) -> &CmdOptions {
&self.options
}
pub fn options_mut(&mut self) -> &mut CmdOptions {
&mut self.options
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct BufferCapacity {
pub(crate) inner: usize,
}
impl TryFrom<usize> for BufferCapacity {
type Error = String;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if value == 0 || value > usize::MAX / 2 {
Err("Buffer capacity must be greater than 0 and less or equal usize::MAX / 2".into())
} else {
Ok(Self { inner: value })
}
}
}
impl AsRef<usize> for BufferCapacity {
fn as_ref(&self) -> &usize {
&self.inner
}
}
impl Default for BufferCapacity {
fn default() -> Self {
Self { inner: 16 }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CmdOptions {
pub(crate) current_dir: Option<PathBuf>,
pub(crate) clear_envs: bool,
pub(crate) envs: HashMap<String, String>,
pub(crate) envs_to_remove: Vec<String>,
pub(crate) output_buffer_capacity: BufferCapacity,
pub(crate) message_input: Option<MessagingType>,
pub(crate) message_output: Option<MessagingType>,
pub(crate) logging_type: Option<LoggingType>,
}
impl CmdOptions {
pub fn with_standard_io_messaging() -> CmdOptions {
Self::with_same_in_out(MessagingType::StandardIo)
}
pub fn with_named_pipe_messaging() -> CmdOptions {
Self::with_same_in_out(MessagingType::NamedPipe)
}
fn with_same_in_out(messaging_type: MessagingType) -> CmdOptions {
CmdOptions {
message_input: messaging_type.clone().into(),
message_output: messaging_type.into(),
..Default::default()
}
}
pub fn with_message_input(message_input: MessagingType) -> Self {
Self {
message_input: message_input.into(),
..Default::default()
}
}
pub fn with_message_output(message_output: MessagingType) -> Self {
Self {
message_output: message_output.into(),
..Default::default()
}
}
pub fn with_logging(logging_type: LoggingType) -> Self {
Self {
logging_type: logging_type.into(),
..Default::default()
}
}
pub fn set_current_dir(&mut self, dir: PathBuf) {
self.current_dir = dir.into();
}
pub fn clear_inherited_envs(&mut self, value: bool) {
self.clear_envs = value;
}
pub fn set_envs<K, V, I>(&mut self, envs: I)
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
self.envs = envs
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
}
pub fn add_env<K, V>(&mut self, name: K, value: V)
where
K: Into<String>,
V: Into<String>,
{
self.envs.insert(name.into(), value.into());
}
pub fn remove_env<S>(&mut self, name: S)
where
S: Into<String> + AsRef<str>,
{
self.envs.remove(name.as_ref());
self.envs_to_remove.push(name.into());
}
pub fn set_message_input(&mut self, messaging_type: MessagingType) {
self.message_input = messaging_type.into();
}
pub fn set_message_output(
&mut self,
messaging_type: MessagingType,
) -> Result<(), CmdOptionsError> {
validate_stdout_config(Some(&messaging_type), self.logging_type.as_ref())?;
self.message_output = messaging_type.into();
Ok(())
}
pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), CmdOptionsError> {
validate_stdout_config(self.message_output.as_ref(), Some(&logging_type))?;
self.logging_type = logging_type.into();
Ok(())
}
pub fn set_message_output_buffer_capacity(&mut self, capacity: BufferCapacity) {
self.output_buffer_capacity = capacity;
}
pub fn current_dir(&self) -> Option<&PathBuf> {
self.current_dir.as_ref()
}
pub fn inherited_envs_cleared(&self) -> bool {
self.clear_envs
}
pub fn envs(&self) -> &HashMap<String, String> {
&self.envs
}
pub fn inherited_envs_to_remove(&self) -> &[String] {
&self.envs_to_remove
}
pub fn message_input(&self) -> Option<&MessagingType> {
self.message_input.as_ref()
}
pub fn message_output(&self) -> Option<&MessagingType> {
self.message_output.as_ref()
}
pub fn logging_type(&self) -> Option<&LoggingType> {
self.logging_type.as_ref()
}
pub fn message_output_buffer_capacity(&self) -> &BufferCapacity {
&self.output_buffer_capacity
}
pub fn update(&mut self, other: CmdOptions) {
if self.current_dir != other.current_dir {
self.current_dir = other.current_dir;
}
if self.clear_envs != other.clear_envs {
self.clear_envs = other.clear_envs;
}
if self.envs != other.envs {
self.envs = other.envs;
}
if self.envs_to_remove != other.envs_to_remove {
for env in other.envs_to_remove {
self.remove_env(env);
}
}
if self.message_input != other.message_input {
self.message_input = other.message_input;
}
if self.message_output != other.message_output {
self.message_output = other.message_output;
}
if self.logging_type != other.logging_type {
self.logging_type = other.logging_type;
}
if self.output_buffer_capacity != other.output_buffer_capacity {
self.output_buffer_capacity = other.output_buffer_capacity;
}
}
}
pub(crate) fn validate_stdout_config(
messaging_type: Option<&MessagingType>,
logging_type: Option<&LoggingType>,
) -> Result<(), CmdOptionsError> {
if let (Some(messaging_type), Some(logging_type)) = (messaging_type, logging_type) {
if messaging_type == &MessagingType::StandardIo && logging_type != &LoggingType::StderrOnly
{
return Err(CmdOptionsError::StdoutConfigurationConflict(
messaging_type.to_owned(),
logging_type.to_owned(),
));
}
}
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum CmdOptionsError {
#[error("Cannot use {0:?} together with {1:?} for stdout configuration")]
StdoutConfigurationConflict(MessagingType, LoggingType),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MessagingType {
StandardIo,
NamedPipe,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LoggingType {
StdoutOnly,
StderrOnly,
StdoutAndStderr,
StdoutAndStderrMerged,
}
impl Runnable for Cmd {
fn bootstrap_cmd(&self, _process_dir: &Path) -> Result<Cmd, String> {
Ok(self.clone())
}
}