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}