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. This module holds
5//! the invocation and the types of its parts.
6//!
7//! An invocation is a value, and nothing starts a program when an application
8//! builds one. An application can therefore describe a command once, write
9//! the command to a log, and name it in an error.
10//!
11//! The [run][run] method starts the command that the invocation describes,
12//! and it reports what the command produced. The [start][start] method starts
13//! the same command and gives its output to the caller while the command
14//! runs.
15//!
16//! [run]: Invocation::run
17//! [start]: Invocation::start
18
19pub mod argument;
20pub mod program;
21pub mod working_directory;
22
23use std::fmt::Display;
24use std::fmt::Formatter;
25use std::fmt::Result as FmtResult;
26use std::process::Stdio;
27use std::time::Instant;
28
29use tokio::process::Command;
30
31pub use self::argument::Argument;
32pub use self::program::Program;
33pub use self::working_directory::WorkingDirectory;
34use crate::error::RunCommandError;
35use crate::execution::Capture;
36use crate::execution::Execution;
37use crate::process_id::ProcessId;
38use crate::run::Run;
39
40/// The description of one external command
41///
42/// An invocation holds the program that the command starts, the arguments of
43/// the program, and the directory in which the command runs. The arguments
44/// keep the order in which the caller named them, and the working directory
45/// is optional.
46///
47/// No shell reads the command. Nothing splits an argument at a space, removes
48/// a quotation mark, or expands a character such as `*`, so the program
49/// receives every argument as the caller wrote it. An application that wants
50/// a shell names the shell as the program, and gives the command line to it
51/// as one argument.
52///
53/// The type renders as a command line through [`Display`], which gives a log
54/// line or an error message the name of the command.
55///
56/// # Examples
57///
58/// A command with two arguments:
59///
60/// ```
61/// use kawauso_process::Invocation;
62///
63/// let invocation = Invocation::new("git").arg("status").arg("--short");
64///
65/// assert_eq!(invocation.to_string(), "git status --short");
66/// ```
67///
68/// A command that runs in a directory of its own:
69///
70/// ```
71/// use std::path::Path;
72///
73/// use kawauso_process::Invocation;
74/// use kawauso_process::invocation::WorkingDirectory;
75///
76/// let invocation = Invocation::new("cargo")
77///     .arg("build")
78///     .in_directory("crates/example");
79///
80/// assert_eq!(
81///     invocation.working_directory().map(WorkingDirectory::get),
82///     Some(Path::new("crates/example"))
83/// );
84/// ```
85#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
86pub struct Invocation {
87    /// The program that the command starts
88    program: Program,
89
90    /// The arguments that the program receives
91    ///
92    /// The order is the order in which the caller named them, because it is
93    /// the order in which the program receives them.
94    arguments: Vec<Argument>,
95
96    /// The directory in which the command runs
97    ///
98    /// `None` when the caller named no directory. The command then runs where
99    /// the process runs.
100    working_directory: Option<WorkingDirectory>,
101}
102
103impl Invocation {
104    /// Describes a command that starts the program that the caller names
105    ///
106    /// The program is the path of an executable file, or the bare name of
107    /// one. The command has no argument yet, and it runs where the process
108    /// runs. [`arg`][arg] adds an argument, and
109    /// [`in_directory`][in-directory] names a working directory.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use kawauso_process::Invocation;
115    ///
116    /// let invocation = Invocation::new("git");
117    ///
118    /// assert_eq!(invocation.program().get(), std::path::Path::new("git"));
119    /// ```
120    ///
121    /// [arg]: Invocation::arg
122    /// [in-directory]: Invocation::in_directory
123    // process[impl invocation.program]
124    pub fn new(program: impl Into<Program>) -> Self {
125        Self {
126            program: program.into(),
127            arguments: Vec::new(),
128            working_directory: None,
129        }
130    }
131
132    /// Adds one argument to the command
133    ///
134    /// Every call adds one argument, and the program receives the arguments
135    /// in the order of the calls. The value reaches the program as the caller
136    /// wrote it, so an argument that holds a space stays one argument.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use kawauso_process::Invocation;
142    ///
143    /// let invocation = Invocation::new("git")
144    ///     .arg("commit")
145    ///     .arg("--message=two words");
146    ///
147    /// assert_eq!(invocation.arguments().len(), 2);
148    /// ```
149    // process[impl invocation.arguments]
150    pub fn arg(mut self, argument: impl Into<Argument>) -> Self {
151        self.arguments.push(argument.into());
152
153        self
154    }
155
156    /// Adds every argument of an iterator to the command
157    ///
158    /// The command keeps the order of the iterator, and a later call appends
159    /// to the arguments that the command holds. Use this method for a caller
160    /// that holds its arguments in a collection, and [`arg`][arg] for one
161    /// that names them.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use kawauso_process::Invocation;
167    ///
168    /// let invocation = Invocation::new("cargo").args(["build", "--release"]);
169    ///
170    /// assert_eq!(invocation.to_string(), "cargo build --release");
171    /// ```
172    ///
173    /// [arg]: Invocation::arg
174    pub fn args(mut self, arguments: impl IntoIterator<Item = impl Into<Argument>>) -> Self {
175        self.arguments.extend(arguments.into_iter().map(Into::into));
176
177        self
178    }
179
180    /// Returns the arguments of the command, in the order of the calls
181    pub fn arguments(&self) -> &[Argument] {
182        &self.arguments
183    }
184
185    /// Builds the command that a run starts
186    ///
187    /// Both forms of a run start the same command, so both take it from here.
188    /// The command pipes both output streams, because a run reports what the
189    /// command wrote to each of them.
190    ///
191    /// The command carries no call that clears or changes the environment, so
192    /// the command inherits the environment of this process. The program
193    /// travels as the caller wrote it, and the operating system resolves a
194    /// bare name.
195    // process[impl run.environment]
196    // process[impl run.resolution]
197    fn command(&self) -> Command {
198        let mut command = Command::new(self.program.get());
199
200        command
201            .args(self.arguments.iter().map(Argument::get))
202            // process[impl run.stdin]
203            .stdin(Stdio::null())
204            .stdout(Stdio::piped())
205            .stderr(Stdio::piped())
206            // process[impl run.abandonment]
207            // process[impl stream.abandonment]
208            .kill_on_drop(true);
209
210        if let Some(directory) = &self.working_directory {
211            command.current_dir(directory.get());
212        }
213
214        command
215    }
216
217    /// Runs the command in the directory that the caller names
218    ///
219    /// Use this method for a command that works on the directory that it runs
220    /// in, such as a build tool that reads the manifest of a project. A
221    /// command without a working directory runs where the process runs.
222    ///
223    /// A later call replaces the directory of an earlier one, because a
224    /// command runs in one directory.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use kawauso_process::Invocation;
230    ///
231    /// let invocation = Invocation::new("cargo")
232    ///     .arg("test")
233    ///     .in_directory("crates/example");
234    ///
235    /// assert!(invocation.working_directory().is_some());
236    /// ```
237    // process[impl invocation.directory]
238    pub fn in_directory(mut self, directory: impl Into<WorkingDirectory>) -> Self {
239        self.working_directory = Some(directory.into());
240
241        self
242    }
243
244    /// Returns the program that the command starts
245    pub fn program(&self) -> &Program {
246        &self.program
247    }
248
249    /// Runs the command and reports what it produced
250    ///
251    /// The method starts the program, reads what the command writes to its
252    /// standard output and to its standard error, and waits for the command
253    /// to end. A command that fills a pipe stops until a reader empties it,
254    /// so the run reads both streams while it waits. A command that writes
255    /// more than a pipe holds therefore never stops the run.
256    ///
257    /// The command starts with a standard input that is null, so a command
258    /// that reads its input sees the end of the input at once. A program that
259    /// asks for a password ends instead of waiting for an answer. The command
260    /// inherits the environment of the process that runs it, and the crate
261    /// has no interface that changes it. The operating system resolves the
262    /// program with the rules of the platform, and the crate searches no path
263    /// of its own.
264    ///
265    /// A command that ends without success is no failure of this method. The
266    /// status travels in the result, and [`require_success`][require-success]
267    /// turns a status that is not a success into an error.
268    ///
269    /// The future of this method owns the command. A caller that drops the
270    /// future, after a timeout for example, ends the command with it.
271    ///
272    /// On Windows, a program whose name ends in `.cmd` or `.bat` starts
273    /// through `cmd.exe`. Rust escapes the arguments for that interpreter and
274    /// refuses an argument that it cannot pass safely, so an argument reaches
275    /// the program as the caller wrote it or the run fails.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`UnstartableCommand`][unstartable] when the program does not
280    /// start, and [`IncompleteRun`][incomplete] when the command started and
281    /// the crate could not collect its result.
282    ///
283    /// # Panics
284    ///
285    /// Panics when no Tokio runtime drives the future. The runtime watches
286    /// the streams and the end of the command, and the crate has no way to
287    /// run the command without one.
288    ///
289    /// # Examples
290    ///
291    /// ```no_run
292    /// use kawauso_process::Invocation;
293    ///
294    /// # #[tokio::main(flavor = "current_thread")]
295    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
296    /// let execution = Invocation::new("git").arg("status").run().await?;
297    ///
298    /// println!("{}", execution.stdout());
299    /// # Ok(())
300    /// # }
301    /// ```
302    ///
303    /// [incomplete]: RunCommandError::IncompleteRun
304    /// [require-success]: Execution::require_success
305    /// [unstartable]: RunCommandError::UnstartableCommand
306    // process[impl run]
307    pub async fn run(&self) -> Result<Execution, RunCommandError> {
308        let start = Instant::now();
309
310        let child =
311            self.command()
312                .spawn()
313                .map_err(|source| RunCommandError::UnstartableCommand {
314                    invocation: self.clone(),
315                    source,
316                })?;
317
318        // The wait takes the command, and the identifier of a command that
319        // ended is gone. The run therefore reads the identifier here, so
320        // that the result can carry it.
321        // process[impl run.identity]
322        let id = child.id().map(ProcessId::from);
323
324        // The wait reads both streams while it waits for the end of the
325        // command. A wait that read one stream after the other, or that read
326        // nothing until the end, would stop on a command that fills a pipe.
327        // process[impl run.drain]
328        let output =
329            child
330                .wait_with_output()
331                .await
332                .map_err(|source| RunCommandError::IncompleteRun {
333                    invocation: self.clone(),
334                    source,
335                })?;
336
337        Ok(Execution::new(
338            self.clone(),
339            id,
340            output.status,
341            Capture::new(output.stdout),
342            Capture::new(output.stderr),
343            start.elapsed(),
344        ))
345    }
346
347    /// Starts the command and reports its output while the command runs
348    ///
349    /// The method starts the program and returns a handle. The handle gives
350    /// the output of the command to the caller as lines, in the order in
351    /// which the lines arrive, and it collects the same capture that
352    /// [`run`][run] collects. Use this method for an application that shows
353    /// the progress of a command, or that turns the output of a command into
354    /// events of its own, and [`run`][run] for one that needs the result
355    /// alone.
356    ///
357    /// The command starts with the same decisions that [`run`][run] makes.
358    /// The standard input is null, the command inherits the environment of
359    /// the process that runs it, and the operating system resolves the
360    /// program.
361    ///
362    /// The handle owns the command. A caller that drops the handle kills the
363    /// command with it, and a caller that asks the command to end first calls
364    /// [`stop`][stop] on the handle.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`UnstartableCommand`][unstartable] when the program does not
369    /// start.
370    ///
371    /// # Panics
372    ///
373    /// Panics when no Tokio runtime runs. The runtime watches the streams and
374    /// the end of the command, and the crate has no way to run the command
375    /// without one.
376    ///
377    /// # Examples
378    ///
379    /// ```no_run
380    /// use kawauso_process::Invocation;
381    ///
382    /// # #[tokio::main(flavor = "current_thread")]
383    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
384    /// let mut run = Invocation::new("cargo").arg("build").start()?;
385    ///
386    /// while let Some(line) = run.next_line().await? {
387    ///     println!("[{}] {line}", line.stream());
388    /// }
389    ///
390    /// println!("{}", run.wait().await?.status());
391    /// # Ok(())
392    /// # }
393    /// ```
394    ///
395    /// [run]: Invocation::run
396    /// [stop]: Run::stop
397    /// [unstartable]: RunCommandError::UnstartableCommand
398    // process[impl stream]
399    pub fn start(&self) -> Result<Run, RunCommandError> {
400        let started = Instant::now();
401
402        let child =
403            self.command()
404                .spawn()
405                .map_err(|source| RunCommandError::UnstartableCommand {
406                    invocation: self.clone(),
407                    source,
408                })?;
409
410        Ok(Run::new(self.clone(), child, started))
411    }
412
413    /// Returns the directory in which the command runs
414    ///
415    /// `None` when the caller named no directory. The command then runs where
416    /// the process runs.
417    pub fn working_directory(&self) -> Option<&WorkingDirectory> {
418        self.working_directory.as_ref()
419    }
420}
421
422/// Renders the command as a command line for a reader
423///
424/// The line names the program, and then every argument in the order in which
425/// the caller named them. It does not name the working directory, because a
426/// command line holds the command and not the place that it runs in.
427///
428/// The line is for a person. No caller reads it back, and no shell runs it,
429/// so it is not a command line that a shell has to accept.
430// process[impl invocation.display]
431impl Display for Invocation {
432    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
433        write_word(formatter, &self.program.get().to_string_lossy())?;
434
435        for argument in &self.arguments {
436            formatter.write_str(" ")?;
437            write_word(formatter, &argument.get().to_string_lossy())?;
438        }
439
440        Ok(())
441    }
442}
443
444/// Writes one word of a command line
445///
446/// A space separates the words of the line, so a word that holds a space
447/// reads as two words, and a word that is empty reads as none. Such a word
448/// goes between quotation marks, and a reader then sees where it starts and
449/// where it ends.
450///
451/// The marks are for the reader. A word that holds a quotation mark of its own
452/// keeps it, because no shell parses the line.
453///
454/// # Errors
455///
456/// Returns the error of the formatter when it cannot take the text.
457fn write_word(formatter: &mut Formatter<'_>, word: &str) -> FmtResult {
458    if word.is_empty() || word.contains(char::is_whitespace) {
459        write!(formatter, "\"{word}\"")
460    } else {
461        formatter.write_str(word)
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    // An assertion in a test panics by design. A `# Panics` section on every
468    // test would repeat that and give the reader no information.
469    #![allow(clippy::missing_panics_doc)]
470
471    use super::*;
472
473    // A caller keeps an invocation in a field of a type that another thread
474    // reads. This test holds the invocation to the auto traits that make this
475    // possible, because a private field of a later version could take them
476    // away without a word from the compiler.
477    #[test]
478    fn invocation_is_send_and_sync() {
479        fn assert_send_and_sync<T: Send + Sync>() {}
480
481        assert_send_and_sync::<Invocation>();
482    }
483}