tokio_process_tools/process/
name.rs1use std::borrow::Cow;
2use typed_builder::TypedBuilder;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum AutoName {
11 Using(AutoNameSettings),
16
17 Debug,
24}
25
26impl AutoName {
27 #[must_use]
31 pub fn program_only() -> Self {
32 Self::Using(AutoNameSettings::program_only())
33 }
34
35 #[must_use]
39 pub fn program_with_args() -> Self {
40 Self::Using(AutoNameSettings::program_with_args())
41 }
42
43 #[must_use]
48 pub fn program_with_env_and_args() -> Self {
49 Self::Using(AutoNameSettings::program_with_env_and_args())
50 }
51
52 #[must_use]
57 pub fn full() -> Self {
58 Self::Using(AutoNameSettings::full())
59 }
60}
61
62impl Default for AutoName {
63 fn default() -> Self {
64 Self::program_only()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, TypedBuilder)]
75#[expect(
76 clippy::struct_excessive_bools,
77 reason = "each flag controls one optional part of the generated process name"
78)]
79pub struct AutoNameSettings {
80 #[builder(default = false)]
81 include_current_dir: bool,
82 #[builder(default = false)]
83 include_envs: bool,
84 #[builder(default = true, setter(skip))]
85 include_program: bool,
86 #[builder(default = false)]
87 include_args: bool,
88}
89
90impl Default for AutoNameSettings {
91 fn default() -> Self {
92 Self {
93 include_current_dir: false,
94 include_envs: false,
95 include_program: true,
96 include_args: false,
97 }
98 }
99}
100
101impl AutoNameSettings {
102 #[must_use]
106 pub fn program_only() -> Self {
107 Self::default()
108 }
109
110 #[must_use]
117 pub fn program_with_args() -> Self {
118 Self::builder().include_args(true).build()
119 }
120
121 #[must_use]
128 pub fn program_with_env_and_args() -> Self {
129 Self::builder()
130 .include_envs(true)
131 .include_args(true)
132 .build()
133 }
134
135 #[must_use]
143 pub fn full() -> Self {
144 Self::builder()
145 .include_current_dir(true)
146 .include_envs(true)
147 .include_args(true)
148 .build()
149 }
150
151 fn format_cmd(self, cmd: &std::process::Command) -> String {
152 let mut name = String::new();
153 if self.include_current_dir
154 && let Some(current_dir) = cmd.get_current_dir()
155 {
156 name.push_str(current_dir.to_string_lossy().as_ref());
157 name.push_str(" % ");
158 }
159 if self.include_envs {
160 for (key, value) in cmd
161 .get_envs()
162 .filter_map(|(key, value)| Some((key, value?)))
163 {
164 name.push_str(key.to_string_lossy().as_ref());
165 name.push('=');
166 name.push_str(value.to_string_lossy().as_ref());
167 name.push(' ');
168 }
169 }
170 if self.include_program {
171 name.push_str(cmd.get_program().to_string_lossy().as_ref());
172 name.push(' ');
173 }
174 if self.include_args {
175 for arg in cmd.get_args() {
176 name.push('"');
177 name.push_str(arg.to_string_lossy().as_ref());
178 name.push('"');
179 name.push(' ');
180 }
181 }
182 if name.ends_with(' ') {
183 name.pop();
184 }
185 name
186 }
187}
188
189#[derive(Debug, Clone)]
198pub enum ProcessName {
199 Explicit(Cow<'static, str>),
203
204 Auto(AutoName),
209}
210
211impl Default for ProcessName {
212 fn default() -> Self {
213 Self::Auto(AutoName::default())
214 }
215}
216
217impl From<&'static str> for ProcessName {
218 fn from(s: &'static str) -> Self {
219 Self::Explicit(Cow::Borrowed(s))
220 }
221}
222
223impl From<String> for ProcessName {
224 fn from(s: String) -> Self {
225 Self::Explicit(Cow::Owned(s))
226 }
227}
228
229impl From<Cow<'static, str>> for ProcessName {
230 fn from(s: Cow<'static, str>) -> Self {
231 Self::Explicit(s)
232 }
233}
234
235impl From<AutoName> for ProcessName {
236 fn from(mode: AutoName) -> Self {
237 Self::Auto(mode)
238 }
239}
240
241impl From<AutoNameSettings> for AutoName {
242 fn from(settings: AutoNameSettings) -> Self {
243 Self::Using(settings)
244 }
245}
246
247impl From<AutoNameSettings> for ProcessName {
248 fn from(settings: AutoNameSettings) -> Self {
249 Self::Auto(settings.into())
250 }
251}
252
253pub(super) fn generate_name(
254 name: &ProcessName,
255 cmd: &tokio::process::Command,
256) -> Cow<'static, str> {
257 match name {
258 ProcessName::Explicit(name) => name.clone(),
259 ProcessName::Auto(auto_name) => match auto_name {
260 AutoName::Using(settings) => settings.format_cmd(cmd.as_std()).into(),
261 AutoName::Debug => format!("{cmd:?}").into(),
262 },
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use assertr::prelude::*;
270 use std::path::PathBuf;
271 use tokio::process::Command;
272
273 fn command_with_args_env_and_current_dir() -> Command {
274 let mut cmd = Command::new("ls");
275 cmd.arg("-la");
276 cmd.env("FOO", "foo");
277 cmd.current_dir(PathBuf::from("./"));
278 cmd
279 }
280
281 #[test]
282 fn auto_name_defaults_to_safe_program_only_naming() {
283 let mut cmd = command_with_args_env_and_current_dir();
284 let sensitive_arg = "--token=secret-token-should-not-be-logged";
285 cmd.arg(sensitive_arg);
286
287 let default_process_name = generated_name(ProcessName::default(), &cmd);
288 let default_auto_name = generated_name(AutoName::default(), &cmd);
289 let program_only_name = generated_name(AutoName::program_only(), &cmd);
290 let builder_default_name = generated_name(AutoNameSettings::builder().build(), &cmd);
291
292 for name in [
293 default_process_name.as_str(),
294 default_auto_name.as_str(),
295 program_only_name.as_str(),
296 builder_default_name.as_str(),
297 ] {
298 assert_that!(name).is_equal_to("ls");
299 assert_that!(name).does_not_contain(sensitive_arg);
300 }
301 }
302
303 #[test]
304 fn auto_name_presets_match_settings_and_expected_output() {
305 let cmd = command_with_args_env_and_current_dir();
306 let cases = [
307 (
308 AutoName::program_only(),
309 AutoNameSettings::program_only(),
310 "ls",
311 ),
312 (
313 AutoName::program_with_args(),
314 AutoNameSettings::program_with_args(),
315 "ls \"-la\"",
316 ),
317 (
318 AutoName::program_with_env_and_args(),
319 AutoNameSettings::program_with_env_and_args(),
320 "FOO=foo ls \"-la\"",
321 ),
322 (
323 AutoName::full(),
324 AutoNameSettings::full(),
325 "./ % FOO=foo ls \"-la\"",
326 ),
327 ];
328
329 for (auto_name, settings, expected) in cases {
330 assert_that!(auto_name).is_equal_to(AutoName::Using(settings));
331 assert_that!(generated_name(auto_name, &cmd)).is_equal_to(expected);
332 assert_that!(generated_name(settings, &cmd)).is_equal_to(expected);
333 }
334 }
335
336 #[test]
337 fn auto_name_debug_uses_command_debug_string() {
338 let cmd = command_with_args_env_and_current_dir();
339
340 assert_that!(generated_name(ProcessName::Auto(AutoName::Debug), &cmd)).is_equal_to(
341 "Command { std: cd \"./\" && FOO=\"foo\" \"ls\" \"-la\", kill_on_drop: false }",
342 );
343 }
344
345 #[test]
346 fn auto_name_settings_builder_supports_custom_combination() {
347 let cmd = command_with_args_env_and_current_dir();
348
349 assert_that!(generated_name(
350 AutoNameSettings::builder()
351 .include_current_dir(true)
352 .include_args(true)
353 .build(),
354 &cmd,
355 ))
356 .is_equal_to("./ % ls \"-la\"");
357 }
358
359 fn generated_name(name: impl Into<ProcessName>, cmd: &Command) -> String {
360 let name = name.into();
361 generate_name(&name, cmd).into_owned()
362 }
363}