use std::borrow::Cow;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoName {
Using(AutoNameSettings),
Debug,
}
impl AutoName {
#[must_use]
pub fn program_only() -> Self {
Self::Using(AutoNameSettings::program_only())
}
#[must_use]
pub fn program_with_args() -> Self {
Self::Using(AutoNameSettings::program_with_args())
}
#[must_use]
pub fn program_with_env_and_args() -> Self {
Self::Using(AutoNameSettings::program_with_env_and_args())
}
#[must_use]
pub fn full() -> Self {
Self::Using(AutoNameSettings::full())
}
}
impl Default for AutoName {
fn default() -> Self {
Self::program_only()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TypedBuilder)]
#[expect(
clippy::struct_excessive_bools,
reason = "each flag controls one optional part of the generated process name"
)]
pub struct AutoNameSettings {
#[builder(default = false)]
include_current_dir: bool,
#[builder(default = false)]
include_envs: bool,
#[builder(default = true, setter(skip))]
include_program: bool,
#[builder(default = false)]
include_args: bool,
}
impl Default for AutoNameSettings {
fn default() -> Self {
Self {
include_current_dir: false,
include_envs: false,
include_program: true,
include_args: false,
}
}
}
impl AutoNameSettings {
#[must_use]
pub fn program_only() -> Self {
Self::default()
}
#[must_use]
pub fn program_with_args() -> Self {
Self::builder().include_args(true).build()
}
#[must_use]
pub fn program_with_env_and_args() -> Self {
Self::builder()
.include_envs(true)
.include_args(true)
.build()
}
#[must_use]
pub fn full() -> Self {
Self::builder()
.include_current_dir(true)
.include_envs(true)
.include_args(true)
.build()
}
fn format_cmd(self, cmd: &std::process::Command) -> String {
let mut name = String::new();
if self.include_current_dir
&& let Some(current_dir) = cmd.get_current_dir()
{
name.push_str(current_dir.to_string_lossy().as_ref());
name.push_str(" % ");
}
if self.include_envs {
for (key, value) in cmd
.get_envs()
.filter_map(|(key, value)| Some((key, value?)))
{
name.push_str(key.to_string_lossy().as_ref());
name.push('=');
name.push_str(value.to_string_lossy().as_ref());
name.push(' ');
}
}
if self.include_program {
name.push_str(cmd.get_program().to_string_lossy().as_ref());
name.push(' ');
}
if self.include_args {
for arg in cmd.get_args() {
name.push('"');
name.push_str(arg.to_string_lossy().as_ref());
name.push('"');
name.push(' ');
}
}
if name.ends_with(' ') {
name.pop();
}
name
}
}
#[derive(Debug, Clone)]
pub enum ProcessName {
Explicit(Cow<'static, str>),
Auto(AutoName),
}
impl Default for ProcessName {
fn default() -> Self {
Self::Auto(AutoName::default())
}
}
impl From<&'static str> for ProcessName {
fn from(s: &'static str) -> Self {
Self::Explicit(Cow::Borrowed(s))
}
}
impl From<String> for ProcessName {
fn from(s: String) -> Self {
Self::Explicit(Cow::Owned(s))
}
}
impl From<Cow<'static, str>> for ProcessName {
fn from(s: Cow<'static, str>) -> Self {
Self::Explicit(s)
}
}
impl From<AutoName> for ProcessName {
fn from(mode: AutoName) -> Self {
Self::Auto(mode)
}
}
impl From<AutoNameSettings> for AutoName {
fn from(settings: AutoNameSettings) -> Self {
Self::Using(settings)
}
}
impl From<AutoNameSettings> for ProcessName {
fn from(settings: AutoNameSettings) -> Self {
Self::Auto(settings.into())
}
}
pub(super) fn generate_name(
name: &ProcessName,
cmd: &tokio::process::Command,
) -> Cow<'static, str> {
match name {
ProcessName::Explicit(name) => name.clone(),
ProcessName::Auto(auto_name) => match auto_name {
AutoName::Using(settings) => settings.format_cmd(cmd.as_std()).into(),
AutoName::Debug => format!("{cmd:?}").into(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use assertr::prelude::*;
use std::path::PathBuf;
use tokio::process::Command;
fn command_with_args_env_and_current_dir() -> Command {
let mut cmd = Command::new("ls");
cmd.arg("-la");
cmd.env("FOO", "foo");
cmd.current_dir(PathBuf::from("./"));
cmd
}
#[test]
fn auto_name_defaults_to_safe_program_only_naming() {
let mut cmd = command_with_args_env_and_current_dir();
let sensitive_arg = "--token=secret-token-should-not-be-logged";
cmd.arg(sensitive_arg);
let default_process_name = generated_name(ProcessName::default(), &cmd);
let default_auto_name = generated_name(AutoName::default(), &cmd);
let program_only_name = generated_name(AutoName::program_only(), &cmd);
let builder_default_name = generated_name(AutoNameSettings::builder().build(), &cmd);
for name in [
default_process_name.as_str(),
default_auto_name.as_str(),
program_only_name.as_str(),
builder_default_name.as_str(),
] {
assert_that!(name).is_equal_to("ls");
assert_that!(name).does_not_contain(sensitive_arg);
}
}
#[test]
fn auto_name_presets_match_settings_and_expected_output() {
let cmd = command_with_args_env_and_current_dir();
let cases = [
(
AutoName::program_only(),
AutoNameSettings::program_only(),
"ls",
),
(
AutoName::program_with_args(),
AutoNameSettings::program_with_args(),
"ls \"-la\"",
),
(
AutoName::program_with_env_and_args(),
AutoNameSettings::program_with_env_and_args(),
"FOO=foo ls \"-la\"",
),
(
AutoName::full(),
AutoNameSettings::full(),
"./ % FOO=foo ls \"-la\"",
),
];
for (auto_name, settings, expected) in cases {
assert_that!(auto_name).is_equal_to(AutoName::Using(settings));
assert_that!(generated_name(auto_name, &cmd)).is_equal_to(expected);
assert_that!(generated_name(settings, &cmd)).is_equal_to(expected);
}
}
#[test]
fn auto_name_debug_uses_command_debug_string() {
let cmd = command_with_args_env_and_current_dir();
assert_that!(generated_name(ProcessName::Auto(AutoName::Debug), &cmd)).is_equal_to(
"Command { std: cd \"./\" && FOO=\"foo\" \"ls\" \"-la\", kill_on_drop: false }",
);
}
#[test]
fn auto_name_settings_builder_supports_custom_combination() {
let cmd = command_with_args_env_and_current_dir();
assert_that!(generated_name(
AutoNameSettings::builder()
.include_current_dir(true)
.include_args(true)
.build(),
&cmd,
))
.is_equal_to("./ % ls \"-la\"");
}
fn generated_name(name: impl Into<ProcessName>, cmd: &Command) -> String {
let name = name.into();
generate_name(&name, cmd).into_owned()
}
}