1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Command construction, configuration, and tracking.

use std::fmt;

use tokio::process::Command as TokioCommand;
use tracing::trace;

use crate::error::RuntimeError;

#[doc(inline)]
pub use process::Process;

#[doc(inline)]
pub use supervisor::Supervisor;

mod process;
mod supervisor;

#[cfg(test)]
mod tests;

/// A command to execute.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Command {
	/// A raw command which will be executed as-is.
	Exec {
		/// The program to run.
		prog: String,

		/// The arguments to pass.
		args: Vec<String>,
	},

	/// A shelled command line.
	Shell {
		/// The shell to run.
		shell: Shell,

		/// Additional options or arguments to pass to the shell.
		///
		/// These will be inserted before the `-c` (or equivalent) option immediately preceding the
		/// command line string.
		args: Vec<String>,

		/// The command line to pass to the shell.
		command: String,
	},
}

/// Shell to use to run shelled commands.
///
/// `Cmd` and `Powershell` are special-cased because they have different calling conventions. Also
/// `Cmd` is only available in Windows, while `Powershell` is also available on unices (provided the
/// end-user has it installed, of course).
///
/// There is no default implemented: as consumer of this library you are encouraged to set your own
/// default as makes sense in your application / for your platform.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Shell {
	/// Use the given string as a unix shell invocation.
	///
	/// This is invoked with `-c` followed by the command.
	Unix(String),

	/// Use the Windows CMD.EXE shell.
	///
	/// This is `cmd.exe` invoked with `/C` followed by the command.
	#[cfg(windows)]
	Cmd,

	/// Use Powershell, on Windows or elsewhere.
	///
	/// This is `powershell.exe` invoked with `-Command` followed by the command on Windows.
	/// On unices, it is equivalent to `Unix("pwsh")`.
	Powershell,
}

impl Command {
	/// Obtain a [`tokio::process::Command`] from a [`Command`].
	///
	/// Behaves as described in the [`Command`] and [`Shell`] documentation.
	///
	/// # Errors
	///
	/// - Errors if the `command` of a `Command::Shell` is empty.
	/// - Errors if the `shell` of a `Shell::Unix(shell)` is empty.
	pub fn to_spawnable(&self) -> Result<TokioCommand, RuntimeError> {
		trace!(cmd=?self, "constructing command");

		match self {
			Self::Exec { prog, args } => {
				let mut c = TokioCommand::new(prog);
				c.args(args);
				Ok(c)
			}

			Self::Shell {
				shell,
				args,
				command,
			} => {
				if command.is_empty() {
					return Err(RuntimeError::CommandShellEmptyCommand);
				}

				let (shcmd, shcliopt) = match shell {
					#[cfg(windows)]
					Shell::Cmd => {
						use std::os::windows::process::CommandExt as _;
						use std::process::Command as StdCommand;

						// TODO this is a workaround until TokioCommand has a raw_arg method. See tokio-rs/tokio#5810.
						let mut std_command = StdCommand::new("cmd.exe");
						std_command.args(args).arg("/C").raw_arg(command);
						return Ok(TokioCommand::from(std_command));
					}

					#[cfg(windows)]
					Shell::Powershell => ("powershell.exe", "-Command"),
					#[cfg(not(windows))]
					Shell::Powershell => ("pwsh", "-c"),

					Shell::Unix(cmd) => {
						if cmd.is_empty() {
							return Err(RuntimeError::CommandShellEmptyShell);
						}

						(cmd.as_str(), "-c")
					}
				};

				let mut c = TokioCommand::new(shcmd);
				c.args(args);
				c.arg(shcliopt).arg(command);
				Ok(c)
			}
		}
	}
}

impl fmt::Display for Command {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::Exec { prog, args } => {
				write!(f, "{prog}")?;
				for arg in args {
					write!(f, " {arg}")?;
				}

				Ok(())
			}
			Self::Shell { command, .. } => {
				write!(f, "{command}")
			}
		}
	}
}