Skip to main content

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}