kawauso_process/invocation/mod.rs
1//! The description of one external command
2//!
3//! An invocation names the program of a command, the arguments of the
4//! program, and the directory in which the command runs. It also carries the
5//! variables that the caller sets in the environment of the command. This
6//! module holds the invocation and the types of its parts.
7//!
8//! An invocation is a value, and nothing starts a program when an application
9//! builds one. An application can therefore describe a command once, write
10//! the command to a log, and name it in an error.
11//!
12//! The [run][run] method starts the command that the invocation describes,
13//! and it reports what the command produced. The [start][start] method starts
14//! the same command and gives its output to the caller while the command
15//! runs.
16//!
17//! [run]: Invocation::run
18//! [start]: Invocation::start
19
20pub mod argument;
21pub mod program;
22pub mod variable;
23pub mod variable_name;
24pub mod variable_value;
25pub mod working_directory;
26
27use std::fmt::Display;
28use std::fmt::Formatter;
29use std::fmt::Result as FmtResult;
30use std::process::Stdio;
31use std::time::Instant;
32
33use tokio::process::Command;
34
35pub use self::argument::Argument;
36pub use self::program::Program;
37pub use self::variable::Variable;
38pub use self::variable_name::VariableName;
39pub use self::variable_value::VariableValue;
40pub use self::working_directory::WorkingDirectory;
41use crate::error::RunCommandError;
42use crate::execution::Capture;
43use crate::execution::Execution;
44use crate::process_id::ProcessId;
45use crate::run::Run;
46
47/// The description of one external command
48///
49/// An invocation holds the program that the command starts, the arguments of
50/// the program, and the directory in which the command runs. The arguments
51/// keep the order in which the caller named them, and the working directory
52/// is optional.
53///
54/// An invocation also holds the variables that the caller sets for the
55/// command. The command inherits the environment of the process that runs
56/// it, and a variable of the invocation replaces a variable of the process
57/// with the same name.
58///
59/// No shell reads the command. Nothing splits an argument at a space, removes
60/// a quotation mark, or expands a character such as `*`, so the program
61/// receives every argument as the caller wrote it. An application that wants
62/// a shell names the shell as the program, and gives the command line to it
63/// as one argument.
64///
65/// The type renders as a command line through [`Display`], which gives a log
66/// line or an error message the name of the command.
67///
68/// # Examples
69///
70/// A command with two arguments:
71///
72/// ```
73/// use kawauso_process::Invocation;
74///
75/// let invocation = Invocation::new("git").arg("status").arg("--short");
76///
77/// assert_eq!(invocation.to_string(), "git status --short");
78/// ```
79///
80/// A command that runs in a directory of its own:
81///
82/// ```
83/// use std::path::Path;
84///
85/// use kawauso_process::Invocation;
86/// use kawauso_process::invocation::WorkingDirectory;
87///
88/// let invocation = Invocation::new("cargo")
89/// .arg("build")
90/// .in_directory("crates/example");
91///
92/// assert_eq!(
93/// invocation.working_directory().map(WorkingDirectory::get),
94/// Some(Path::new("crates/example"))
95/// );
96/// ```
97///
98/// A command that reads a variable of its environment:
99///
100/// ```
101/// use kawauso_process::Invocation;
102///
103/// let invocation = Invocation::new("cargo")
104/// .arg("nextest")
105/// .arg("run")
106/// .env("NEXTEST_EXPERIMENTAL_LIBTEST_JSON", "1");
107///
108/// assert_eq!(
109/// invocation.to_string(),
110/// "NEXTEST_EXPERIMENTAL_LIBTEST_JSON=1 cargo nextest run"
111/// );
112/// ```
113#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
114pub struct Invocation {
115 /// The program that the command starts
116 program: Program,
117
118 /// The arguments that the program receives
119 ///
120 /// The order is the order in which the caller named them, because it is
121 /// the order in which the program receives them.
122 arguments: Vec<Argument>,
123
124 /// The variables that the caller sets in the environment of the command
125 ///
126 /// The order is the order in which the caller set them, and a name
127 /// appears once. The command inherits the environment of the process that
128 /// runs it, and these variables come on top of that environment.
129 environment: Vec<Variable>,
130
131 /// The names that the caller takes out of the environment of the command
132 ///
133 /// The order is the order in which the caller named them, and a name
134 /// appears once. A name of this list appears in no variable of the
135 /// environment above, because a name is either set or taken out.
136 removals: Vec<VariableName>,
137
138 /// The directory in which the command runs
139 ///
140 /// `None` when the caller named no directory. The command then runs where
141 /// the process runs.
142 working_directory: Option<WorkingDirectory>,
143}
144
145impl Invocation {
146 /// Describes a command that starts the program that the caller names
147 ///
148 /// The program is the path of an executable file, or the bare name of
149 /// one. The command has no argument yet, and it runs where the process
150 /// runs. [`arg`][arg] adds an argument, and
151 /// [`in_directory`][in-directory] names a working directory.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// use kawauso_process::Invocation;
157 ///
158 /// let invocation = Invocation::new("git");
159 ///
160 /// assert_eq!(invocation.program().get(), std::path::Path::new("git"));
161 /// ```
162 ///
163 /// [arg]: Invocation::arg
164 /// [in-directory]: Invocation::in_directory
165 // process[impl invocation.program]
166 pub fn new(program: impl Into<Program>) -> Self {
167 Self {
168 program: program.into(),
169 arguments: Vec::new(),
170 environment: Vec::new(),
171 removals: Vec::new(),
172 working_directory: None,
173 }
174 }
175
176 /// Adds one argument to the command
177 ///
178 /// Every call adds one argument, and the program receives the arguments
179 /// in the order of the calls. The value reaches the program as the caller
180 /// wrote it, so an argument that holds a space stays one argument.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use kawauso_process::Invocation;
186 ///
187 /// let invocation = Invocation::new("git")
188 /// .arg("commit")
189 /// .arg("--message=two words");
190 ///
191 /// assert_eq!(invocation.arguments().len(), 2);
192 /// ```
193 // process[impl invocation.arguments]
194 pub fn arg(mut self, argument: impl Into<Argument>) -> Self {
195 self.arguments.push(argument.into());
196
197 self
198 }
199
200 /// Adds every argument of an iterator to the command
201 ///
202 /// The command keeps the order of the iterator, and a later call appends
203 /// to the arguments that the command holds. Use this method for a caller
204 /// that holds its arguments in a collection, and [`arg`][arg] for one
205 /// that names them.
206 ///
207 /// # Examples
208 ///
209 /// ```
210 /// use kawauso_process::Invocation;
211 ///
212 /// let invocation = Invocation::new("cargo").args(["build", "--release"]);
213 ///
214 /// assert_eq!(invocation.to_string(), "cargo build --release");
215 /// ```
216 ///
217 /// [arg]: Invocation::arg
218 pub fn args(mut self, arguments: impl IntoIterator<Item = impl Into<Argument>>) -> Self {
219 self.arguments.extend(arguments.into_iter().map(Into::into));
220
221 self
222 }
223
224 /// Returns the arguments of the command, in the order of the calls
225 pub fn arguments(&self) -> &[Argument] {
226 &self.arguments
227 }
228
229 /// Builds the command that a run starts
230 ///
231 /// Both forms of a run start the same command, so both take it from here.
232 /// The command pipes both output streams, because a run reports what the
233 /// command wrote to each of them.
234 ///
235 /// The command carries no call that clears the environment, so the
236 /// command inherits the environment of this process. The variables of the
237 /// invocation come on top of it, and a variable with the name of one that
238 /// the process holds replaces it for the command alone. The program
239 /// travels as the caller wrote it, and the operating system resolves a
240 /// bare name.
241 // process[impl run.environment]
242 // process[impl run.resolution]
243 fn command(&self) -> Command {
244 let mut command = Command::new(self.program.get());
245
246 command
247 .args(self.arguments.iter().map(Argument::get))
248 // process[impl invocation.environment]
249 .envs(
250 self.environment
251 .iter()
252 .map(|variable| (variable.name().get(), variable.value().get())),
253 )
254 // process[impl run.stdin]
255 .stdin(Stdio::null())
256 .stdout(Stdio::piped())
257 .stderr(Stdio::piped())
258 // process[impl run.abandonment]
259 // process[impl stream.abandonment]
260 .kill_on_drop(true);
261
262 // process[impl invocation.environment.removal]
263 for name in &self.removals {
264 command.env_remove(name.get());
265 }
266
267 if let Some(directory) = &self.working_directory {
268 command.current_dir(directory.get());
269 }
270
271 command
272 }
273
274 /// Sets one variable in the environment of the command
275 ///
276 /// The command inherits the environment of the process that runs it, and
277 /// the variable comes on top of that environment. A variable with the
278 /// name of one that the process holds replaces the value for the command
279 /// alone, and the process keeps its own.
280 ///
281 /// A later call with the same name replaces the value of the earlier
282 /// call, because a variable holds one value. The variable keeps the place
283 /// of the earlier call.
284 ///
285 /// # Examples
286 ///
287 /// ```
288 /// use kawauso_process::Invocation;
289 ///
290 /// let invocation = Invocation::new("cargo")
291 /// .arg("build")
292 /// .env("RUSTUP_TOOLCHAIN", "nightly");
293 ///
294 /// assert_eq!(invocation.environment().len(), 1);
295 /// ```
296 // process[impl invocation.environment]
297 pub fn env(mut self, name: impl Into<VariableName>, value: impl Into<VariableValue>) -> Self {
298 self.set(Variable::new(name, value));
299
300 self
301 }
302
303 /// Returns the variables that the caller set, in the order of the calls
304 pub fn environment(&self) -> &[Variable] {
305 &self.environment
306 }
307
308 /// Takes one name out of the environment of the command
309 ///
310 /// The command inherits the environment of the process that runs it, and
311 /// this name is the exception: the command runs as if the process never
312 /// held it. Use this method for a program that reads a variable which the
313 /// caller cannot set to a value that would be right, such as one that
314 /// names the repository of a version control system to a child of a hook.
315 ///
316 /// The process keeps its own environment. Nothing here changes the
317 /// variables that this process holds, which is what makes the method safe
318 /// in a program that runs several commands at once.
319 ///
320 /// A name is either set or taken out, so this call undoes an earlier
321 /// [`env`][env] with the same name, and a later [`env`][env] with the name
322 /// undoes this call. A repeated call keeps the place of the earlier one.
323 ///
324 /// # Examples
325 ///
326 /// ```
327 /// use kawauso_process::Invocation;
328 ///
329 /// let invocation = Invocation::new("git")
330 /// .arg("status")
331 /// .env_remove("GIT_DIR");
332 ///
333 /// assert_eq!(invocation.to_string(), "-GIT_DIR git status");
334 /// ```
335 ///
336 /// [env]: Invocation::env
337 // process[impl invocation.environment.removal]
338 // process[impl invocation.environment.exclusion]
339 pub fn env_remove(mut self, name: impl Into<VariableName>) -> Self {
340 let name = name.into();
341
342 self.environment.retain(|variable| *variable.name() != name);
343
344 if !self.removals.contains(&name) {
345 self.removals.push(name);
346 }
347
348 self
349 }
350
351 /// Returns the names that the caller took out, in the order of the calls
352 pub fn removals(&self) -> &[VariableName] {
353 &self.removals
354 }
355
356 /// Sets every variable of an iterator in the environment of the command
357 ///
358 /// Each variable goes through the same steps as one that [`env`][env]
359 /// sets, so a later variable with the name of an earlier one replaces its
360 /// value. A pair of a name and a value is a variable, so a caller that
361 /// holds its variables as pairs gives them as they are. Use this method
362 /// for a caller that holds its variables in a collection, and
363 /// [`env`][env] for one that names them.
364 ///
365 /// # Examples
366 ///
367 /// ```
368 /// use kawauso_process::Invocation;
369 ///
370 /// let invocation = Invocation::new("cargo")
371 /// .arg("build")
372 /// .envs([("RUSTUP_TOOLCHAIN", "nightly"), ("CARGO_TERM_COLOR", "always")]);
373 ///
374 /// assert_eq!(invocation.environment().len(), 2);
375 /// ```
376 ///
377 /// [env]: Invocation::env
378 // process[impl invocation.environment]
379 pub fn envs(mut self, variables: impl IntoIterator<Item = impl Into<Variable>>) -> Self {
380 for variable in variables {
381 self.set(variable.into());
382 }
383
384 self
385 }
386
387 /// Runs the command in the directory that the caller names
388 ///
389 /// Use this method for a command that works on the directory that it runs
390 /// in, such as a build tool that reads the manifest of a project. A
391 /// command without a working directory runs where the process runs.
392 ///
393 /// A later call replaces the directory of an earlier one, because a
394 /// command runs in one directory.
395 ///
396 /// # Examples
397 ///
398 /// ```
399 /// use kawauso_process::Invocation;
400 ///
401 /// let invocation = Invocation::new("cargo")
402 /// .arg("test")
403 /// .in_directory("crates/example");
404 ///
405 /// assert!(invocation.working_directory().is_some());
406 /// ```
407 // process[impl invocation.directory]
408 pub fn in_directory(mut self, directory: impl Into<WorkingDirectory>) -> Self {
409 self.working_directory = Some(directory.into());
410
411 self
412 }
413
414 /// Returns the program that the command starts
415 pub fn program(&self) -> &Program {
416 &self.program
417 }
418
419 /// Runs the command and reports what it produced
420 ///
421 /// The method starts the program, reads what the command writes to its
422 /// standard output and to its standard error, and waits for the command
423 /// to end. A command that fills a pipe stops until a reader empties it,
424 /// so the run reads both streams while it waits. A command that writes
425 /// more than a pipe holds therefore never stops the run.
426 ///
427 /// The command starts with a standard input that is null, so a command
428 /// that reads its input sees the end of the input at once. A program that
429 /// asks for a password ends instead of waiting for an answer. The command
430 /// inherits the environment of the process that runs it, and it reads the
431 /// variables of the invocation on top of that environment. The operating
432 /// system resolves the program with the rules of the platform, and the
433 /// crate searches no path of its own.
434 ///
435 /// A command that ends without success is no failure of this method. The
436 /// status travels in the result, and [`require_success`][require-success]
437 /// turns a status that is not a success into an error.
438 ///
439 /// The future of this method owns the command. A caller that drops the
440 /// future, after a timeout for example, ends the command with it.
441 ///
442 /// On Windows, a program whose name ends in `.cmd` or `.bat` starts
443 /// through `cmd.exe`. Rust escapes the arguments for that interpreter and
444 /// refuses an argument that it cannot pass safely, so an argument reaches
445 /// the program as the caller wrote it or the run fails.
446 ///
447 /// # Errors
448 ///
449 /// Returns [`UnstartableCommand`][unstartable] when the program does not
450 /// start, and [`IncompleteRun`][incomplete] when the command started and
451 /// the crate could not collect its result.
452 ///
453 /// # Panics
454 ///
455 /// Panics when no Tokio runtime drives the future. The runtime watches
456 /// the streams and the end of the command, and the crate has no way to
457 /// run the command without one.
458 ///
459 /// # Examples
460 ///
461 /// ```no_run
462 /// use kawauso_process::Invocation;
463 ///
464 /// # #[tokio::main(flavor = "current_thread")]
465 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
466 /// let execution = Invocation::new("git").arg("status").run().await?;
467 ///
468 /// println!("{}", execution.stdout());
469 /// # Ok(())
470 /// # }
471 /// ```
472 ///
473 /// [incomplete]: RunCommandError::IncompleteRun
474 /// [require-success]: Execution::require_success
475 /// [unstartable]: RunCommandError::UnstartableCommand
476 // process[impl run]
477 pub async fn run(&self) -> Result<Execution, RunCommandError> {
478 let start = Instant::now();
479
480 let child =
481 self.command()
482 .spawn()
483 .map_err(|source| RunCommandError::UnstartableCommand {
484 invocation: Box::new(self.clone()),
485 source,
486 })?;
487
488 // The wait takes the command, and the identifier of a command that
489 // ended is gone. The run therefore reads the identifier here, so
490 // that the result can carry it.
491 // process[impl run.identity]
492 let id = child.id().map(ProcessId::from);
493
494 // The wait reads both streams while it waits for the end of the
495 // command. A wait that read one stream after the other, or that read
496 // nothing until the end, would stop on a command that fills a pipe.
497 // process[impl run.drain]
498 let output =
499 child
500 .wait_with_output()
501 .await
502 .map_err(|source| RunCommandError::IncompleteRun {
503 invocation: Box::new(self.clone()),
504 source,
505 })?;
506
507 Ok(Execution::new(
508 self.clone(),
509 id,
510 output.status,
511 Capture::new(output.stdout),
512 Capture::new(output.stderr),
513 start.elapsed(),
514 ))
515 }
516
517 /// Puts one variable into the environment of the command
518 ///
519 /// A variable with a name that the environment holds replaces the value
520 /// of that variable and keeps its place, because a variable holds one
521 /// value. A variable with a new name goes to the end, so that the
522 /// environment keeps the order of the calls.
523 // process[impl invocation.environment.replacement]
524 // process[impl invocation.environment.exclusion]
525 fn set(&mut self, variable: Variable) {
526 self.removals.retain(|name| name != variable.name());
527
528 let existing = self
529 .environment
530 .iter_mut()
531 .find(|existing| existing.name() == variable.name());
532
533 match existing {
534 Some(existing) => *existing = variable,
535 None => self.environment.push(variable),
536 }
537 }
538
539 /// Starts the command and reports its output while the command runs
540 ///
541 /// The method starts the program and returns a handle. The handle gives
542 /// the output of the command to the caller as lines, in the order in
543 /// which the lines arrive, and it collects the same capture that
544 /// [`run`][run] collects. Use this method for an application that shows
545 /// the progress of a command, or that turns the output of a command into
546 /// events of its own, and [`run`][run] for one that needs the result
547 /// alone.
548 ///
549 /// The command starts with the same decisions that [`run`][run] makes.
550 /// The standard input is null, the command inherits the environment of
551 /// the process that runs it, and the operating system resolves the
552 /// program. The variables of the invocation reach the command as well.
553 ///
554 /// The handle owns the command. A caller that drops the handle kills the
555 /// command with it, and a caller that asks the command to end first calls
556 /// [`stop`][stop] on the handle.
557 ///
558 /// # Errors
559 ///
560 /// Returns [`UnstartableCommand`][unstartable] when the program does not
561 /// start.
562 ///
563 /// # Panics
564 ///
565 /// Panics when no Tokio runtime runs. The runtime watches the streams and
566 /// the end of the command, and the crate has no way to run the command
567 /// without one.
568 ///
569 /// # Examples
570 ///
571 /// ```no_run
572 /// use kawauso_process::Invocation;
573 ///
574 /// # #[tokio::main(flavor = "current_thread")]
575 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
576 /// let mut run = Invocation::new("cargo").arg("build").start()?;
577 ///
578 /// while let Some(line) = run.next_line().await? {
579 /// println!("[{}] {line}", line.stream());
580 /// }
581 ///
582 /// println!("{}", run.wait().await?.status());
583 /// # Ok(())
584 /// # }
585 /// ```
586 ///
587 /// [run]: Invocation::run
588 /// [stop]: Run::stop
589 /// [unstartable]: RunCommandError::UnstartableCommand
590 // process[impl stream]
591 pub fn start(&self) -> Result<Run, RunCommandError> {
592 let started = Instant::now();
593
594 let child =
595 self.command()
596 .spawn()
597 .map_err(|source| RunCommandError::UnstartableCommand {
598 invocation: Box::new(self.clone()),
599 source,
600 })?;
601
602 Ok(Run::new(self.clone(), child, started))
603 }
604
605 /// Returns the directory in which the command runs
606 ///
607 /// `None` when the caller named no directory. The command then runs where
608 /// the process runs.
609 pub fn working_directory(&self) -> Option<&WorkingDirectory> {
610 self.working_directory.as_ref()
611 }
612}
613
614/// Renders the command as a command line for a reader
615///
616/// The line names every name that the caller took out, then every variable
617/// that the caller set, then the program, and then every argument, each in the
618/// order in which the caller named them. A variable shows as its name, an
619/// equals sign, and its value, which is the form in which a shell sets a
620/// variable for one command. A name that the caller took out shows behind a
621/// minus sign, which is the form of the `env` program, because no shell takes
622/// a name out of the environment of one command. The line does not name the
623/// working directory, because a command line holds the command and not the
624/// place that it runs in.
625///
626/// The line is for a person. No caller reads it back, and no shell runs it,
627/// so it is not a command line that a shell has to accept.
628// process[impl invocation.display]
629// process[impl invocation.environment.display]
630// process[impl invocation.environment.removal.display]
631impl Display for Invocation {
632 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
633 // process[impl invocation.environment.removal.display]
634 for name in &self.removals {
635 write!(formatter, "-{name} ")?;
636 }
637
638 for variable in &self.environment {
639 write!(formatter, "{}=", variable.name())?;
640 write_word(formatter, &variable.value().get().to_string_lossy())?;
641 formatter.write_str(" ")?;
642 }
643
644 write_word(formatter, &self.program.get().to_string_lossy())?;
645
646 for argument in &self.arguments {
647 formatter.write_str(" ")?;
648 write_word(formatter, &argument.get().to_string_lossy())?;
649 }
650
651 Ok(())
652 }
653}
654
655/// Writes one word of a command line
656///
657/// A space separates the words of the line, so a word that holds a space
658/// reads as two words, and a word that is empty reads as none. Such a word
659/// goes between quotation marks, and a reader then sees where it starts and
660/// where it ends.
661///
662/// The value of a variable is a word of this kind as well. The marks go around
663/// the value alone, and not around the name, which is how a shell writes a
664/// variable whose value holds a space.
665///
666/// The marks are for the reader. A word that holds a quotation mark of its own
667/// keeps it, because no shell parses the line.
668///
669/// # Errors
670///
671/// Returns the error of the formatter when it cannot take the text.
672fn write_word(formatter: &mut Formatter<'_>, word: &str) -> FmtResult {
673 if word.is_empty() || word.contains(char::is_whitespace) {
674 write!(formatter, "\"{word}\"")
675 } else {
676 formatter.write_str(word)
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 // An assertion in a test panics by design. A `# Panics` section on every
683 // test would repeat that and give the reader no information.
684 #![allow(clippy::missing_panics_doc)]
685
686 use super::*;
687
688 // A caller keeps an invocation in a field of a type that another thread
689 // reads. This test holds the invocation to the auto traits that make this
690 // possible, because a private field of a later version could take them
691 // away without a word from the compiler.
692 #[test]
693 fn invocation_is_send_and_sync() {
694 fn assert_send_and_sync<T: Send + Sync>() {}
695
696 assert_send_and_sync::<Invocation>();
697 }
698}