openssh/command.rs
1use crate::escape::escape;
2
3use super::child::Child;
4use super::stdio::TryFromChildIo;
5use super::Stdio;
6use super::{Error, Session};
7
8use std::borrow::Cow;
9use std::ffi::OsStr;
10use std::ops::Deref;
11use std::process;
12
13#[derive(Debug)]
14pub(crate) enum CommandImp {
15 #[cfg(feature = "process-mux")]
16 ProcessImpl(super::process_impl::Command),
17
18 #[cfg(feature = "native-mux")]
19 NativeMuxImpl(super::native_mux_impl::Command),
20}
21#[cfg(feature = "process-mux")]
22impl From<super::process_impl::Command> for CommandImp {
23 fn from(imp: super::process_impl::Command) -> Self {
24 CommandImp::ProcessImpl(imp)
25 }
26}
27
28#[cfg(feature = "native-mux")]
29impl From<super::native_mux_impl::Command> for CommandImp {
30 fn from(imp: super::native_mux_impl::Command) -> Self {
31 CommandImp::NativeMuxImpl(imp)
32 }
33}
34
35#[cfg(any(feature = "process-mux", feature = "native-mux"))]
36macro_rules! delegate {
37 ($impl:expr, $var:ident, $then:block) => {{
38 match $impl {
39 #[cfg(feature = "process-mux")]
40 CommandImp::ProcessImpl($var) => $then,
41
42 #[cfg(feature = "native-mux")]
43 CommandImp::NativeMuxImpl($var) => $then,
44 }
45 }};
46}
47
48#[cfg(not(any(feature = "process-mux", feature = "native-mux")))]
49macro_rules! delegate {
50 ($impl:expr, $var:ident, $then:block) => {{
51 unreachable!("Neither feature process-mux nor native-mux is enabled")
52 }};
53}
54
55/// If a command is `OverSsh` then it can be executed over an SSH session.
56///
57/// Primarily a way to allow `std::process::Command` to be turned directly into an `openssh::Command`.
58pub trait OverSsh {
59 /// Given an ssh session, return a command that can be executed over that ssh session.
60 ///
61 /// ### Notes
62 ///
63 /// The command to be executed on the remote machine should not explicitly
64 /// set environment variables or the current working directory. It errors if the source command
65 /// has environment variables or a current working directory set, since `openssh` doesn't (yet) have
66 /// a method to set environment variables and `ssh` doesn't support setting a current working directory
67 /// outside of `bash/dash/zsh` (which is not always available).
68 ///
69 /// ### Examples
70 ///
71 /// 1. Consider the implementation of `OverSsh` for `std::process::Command`. Let's build a
72 /// `ls -l -a -h` command and execute it over an SSH session.
73 ///
74 /// ```no_run
75 /// # #[tokio::main(flavor = "current_thread")]
76 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
77 /// use std::process::Command;
78 /// use openssh::{Session, KnownHosts, OverSsh};
79 ///
80 /// let session = Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?;
81 /// let ls =
82 /// Command::new("ls")
83 /// .arg("-l")
84 /// .arg("-a")
85 /// .arg("-h")
86 /// .over_ssh(&session)?
87 /// .output()
88 /// .await?;
89 ///
90 /// assert!(String::from_utf8(ls.stdout).unwrap().contains("total"));
91 /// # Ok(())
92 /// }
93 ///
94 /// ```
95 /// 2. Building a command with environment variables or a current working directory set will
96 /// results in an error.
97 ///
98 /// ```no_run
99 /// # #[tokio::main(flavor = "current_thread")]
100 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
101 /// use std::process::Command;
102 /// use openssh::{Session, KnownHosts, OverSsh};
103 ///
104 /// let session = Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?;
105 /// let echo =
106 /// Command::new("echo")
107 /// .env("MY_ENV_VAR", "foo")
108 /// .arg("$MY_ENV_VAR")
109 /// .over_ssh(&session);
110 /// assert!(matches!(echo, Err(openssh::Error::CommandHasEnv)));
111 ///
112 /// # Ok(())
113 /// }
114 ///
115 /// ```
116 fn over_ssh<S: Deref<Target = Session> + Clone>(
117 &self,
118 session: S,
119 ) -> Result<OwningCommand<S>, crate::Error>;
120}
121
122impl OverSsh for std::process::Command {
123 fn over_ssh<S: Deref<Target = Session> + Clone>(
124 &self,
125 session: S,
126 ) -> Result<OwningCommand<S>, crate::Error> {
127 // I'd really like `!self.get_envs().is_empty()` here, but that's
128 // behind a `exact_size_is_empty` feature flag.
129 if self.get_envs().len() > 0 {
130 return Err(crate::Error::CommandHasEnv);
131 }
132
133 if self.get_current_dir().is_some() {
134 return Err(crate::Error::CommandHasCwd);
135 }
136
137 let program_escaped: Cow<'_, OsStr> = escape(self.get_program());
138 let mut command = Session::to_raw_command(session, program_escaped);
139
140 let args = self.get_args().map(escape);
141 command.raw_args(args);
142 Ok(command)
143 }
144}
145
146impl OverSsh for tokio::process::Command {
147 fn over_ssh<S: Deref<Target = Session> + Clone>(
148 &self,
149 session: S,
150 ) -> Result<OwningCommand<S>, crate::Error> {
151 self.as_std().over_ssh(session)
152 }
153}
154
155impl<S> OverSsh for &S
156where
157 S: OverSsh,
158{
159 fn over_ssh<U: Deref<Target = Session> + Clone>(
160 &self,
161 session: U,
162 ) -> Result<OwningCommand<U>, crate::Error> {
163 <S as OverSsh>::over_ssh(self, session)
164 }
165}
166
167impl<S> OverSsh for &mut S
168where
169 S: OverSsh,
170{
171 fn over_ssh<U: Deref<Target = Session> + Clone>(
172 &self,
173 session: U,
174 ) -> Result<OwningCommand<U>, crate::Error> {
175 <S as OverSsh>::over_ssh(self, session)
176 }
177}
178
179/// A remote process builder, providing fine-grained control over how a new remote process should
180/// be spawned.
181///
182/// A default configuration can be generated using [`Session::command(program)`](Session::command)
183/// or [`Session::arc_command(program)`](Session::arc_command), where `program` gives a path to
184/// the program to be executed. Additional builder methods allow the configuration to be changed
185/// (for example, by adding arguments) prior to spawning. The interface is almost identical to
186/// that of [`std::process::Command`].
187///
188/// `OwningCommand` can be reused to spawn multiple remote processes. The builder methods change
189/// the command without needing to immediately spawn the process. Similarly, you can call builder
190/// methods after spawning a process and then spawn a new process with the modified settings.
191///
192/// # Environment variables and current working directory.
193///
194/// You'll notice that unlike its `std` counterpart, `OwningCommand` does not have any methods for
195/// setting environment variables or the current working directory for the remote command. This is
196/// because the SSH protocol does not support this (at least not in its standard configuration).
197/// For more details on this, see the `ENVIRONMENT` section of [`ssh(1)`]. To work around this,
198/// give [`env(1)`] a try. If the remote shell supports it, you can also prefix your command with
199/// `["cd", "dir", "&&"]` to run the rest of the command in some directory `dir`.
200///
201/// # Exit status
202///
203/// The `ssh` command generally forwards the exit status of the remote process. The exception is if
204/// a protocol-level error occured, in which case it will return with exit status 255. Since the
205/// remote process _could_ also return with exit status 255, we have no reliable way to distinguish
206/// between remote errors and errors from `ssh`, but this library _assumes_ that 255 means the
207/// error came from `ssh`, and acts accordingly.
208///
209/// [`ssh(1)`]: https://linux.die.net/man/1/ssh
210/// [`env(1)`]: https://linux.die.net/man/1/env
211#[derive(Debug)]
212pub struct OwningCommand<S> {
213 session: S,
214 imp: CommandImp,
215
216 stdin_set: bool,
217 stdout_set: bool,
218 stderr_set: bool,
219}
220
221impl<S> OwningCommand<S> {
222 pub(crate) fn new(session: S, imp: CommandImp) -> Self {
223 Self {
224 session,
225 imp,
226
227 stdin_set: false,
228 stdout_set: false,
229 stderr_set: false,
230 }
231 }
232
233 /// Adds an argument to pass to the remote program.
234 ///
235 /// Before it is passed to the remote host, `arg` is escaped so that special characters aren't
236 /// evaluated by the remote shell. If you do not want this behavior, use
237 /// [`raw_arg`](Self::raw_arg).
238 ///
239 /// Only one argument can be passed per use. So instead of:
240 ///
241 /// ```no_run
242 /// # fn foo(c: &mut openssh::Command<'_>) { c
243 /// .arg("-C /path/to/repo")
244 /// # ; }
245 /// ```
246 ///
247 /// usage would be:
248 ///
249 /// ```no_run
250 /// # fn foo(c: &mut openssh::Command<'_>) { c
251 /// .arg("-C")
252 /// .arg("/path/to/repo")
253 /// # ; }
254 /// ```
255 ///
256 /// To pass multiple arguments see [`args`](Self::args).
257 pub fn arg<A: AsRef<str>>(&mut self, arg: A) -> &mut Self {
258 self.raw_arg(&*shell_escape::unix::escape(Cow::Borrowed(arg.as_ref())))
259 }
260
261 /// Adds an argument to pass to the remote program.
262 ///
263 /// Unlike [`arg`](Self::arg), this method does not shell-escape `arg`. The argument is passed as written
264 /// to `ssh`, which will pass it again as an argument to the remote shell. Since the remote
265 /// shell may do argument parsing, characters such as spaces and `*` may be interpreted by the
266 /// remote shell.
267 ///
268 /// To pass multiple unescaped arguments see [`raw_args`](Self::raw_args).
269 pub fn raw_arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
270 delegate!(&mut self.imp, imp, {
271 imp.raw_arg(arg.as_ref());
272 });
273 self
274 }
275
276 /// Adds multiple arguments to pass to the remote program.
277 ///
278 /// Before they are passed to the remote host, each argument in `args` is escaped so that
279 /// special characters aren't evaluated by the remote shell. If you do not want this behavior,
280 /// use [`raw_args`](Self::raw_args).
281 ///
282 /// To pass a single argument see [`arg`](Self::arg).
283 pub fn args<I, A>(&mut self, args: I) -> &mut Self
284 where
285 I: IntoIterator<Item = A>,
286 A: AsRef<str>,
287 {
288 for arg in args {
289 self.arg(arg);
290 }
291 self
292 }
293
294 /// Adds multiple arguments to pass to the remote program.
295 ///
296 /// Unlike [`args`](Self::args), this method does not shell-escape `args`. The arguments are passed as
297 /// written to `ssh`, which will pass them again as arguments to the remote shell. However,
298 /// since the remote shell may do argument parsing, characters such as spaces and `*` may be
299 /// interpreted by the remote shell.
300 ///
301 /// To pass a single argument see [`raw_arg`](Self::raw_arg).
302 pub fn raw_args<I, A>(&mut self, args: I) -> &mut Self
303 where
304 I: IntoIterator<Item = A>,
305 A: AsRef<OsStr>,
306 {
307 for arg in args {
308 self.raw_arg(arg);
309 }
310 self
311 }
312
313 /// Configuration for the remote process's standard input (stdin) handle.
314 ///
315 /// Defaults to [`inherit`] when used with `spawn` or `status`, and
316 /// defaults to [`null`] when used with `output`.
317 ///
318 /// [`inherit`]: struct.Stdio.html#method.inherit
319 /// [`null`]: struct.Stdio.html#method.null
320 pub fn stdin<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
321 delegate!(&mut self.imp, imp, {
322 imp.stdin(cfg.into());
323 });
324 self.stdin_set = true;
325 self
326 }
327
328 /// Configuration for the remote process's standard output (stdout) handle.
329 ///
330 /// Defaults to [`inherit`] when used with `spawn` or `status`, and
331 /// defaults to [`piped`] when used with `output`.
332 ///
333 /// [`inherit`]: struct.Stdio.html#method.inherit
334 /// [`piped`]: struct.Stdio.html#method.piped
335 pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
336 delegate!(&mut self.imp, imp, {
337 imp.stdout(cfg.into());
338 });
339 self.stdout_set = true;
340 self
341 }
342
343 /// Configuration for the remote process's standard error (stderr) handle.
344 ///
345 /// Defaults to [`inherit`] when used with `spawn` or `status`, and
346 /// defaults to [`piped`] when used with `output`.
347 ///
348 /// [`inherit`]: struct.Stdio.html#method.inherit
349 /// [`piped`]: struct.Stdio.html#method.piped
350 pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
351 delegate!(&mut self.imp, imp, {
352 imp.stderr(cfg.into());
353 });
354 self.stderr_set = true;
355 self
356 }
357}
358
359impl<S: Clone> OwningCommand<S> {
360 async fn spawn_impl(&mut self) -> Result<Child<S>, Error> {
361 Ok(Child::new(
362 self.session.clone(),
363 delegate!(&mut self.imp, imp, {
364 let (imp, stdin, stdout, stderr) = imp.spawn().await?;
365 (
366 imp.into(),
367 stdin.map(TryFromChildIo::try_from).transpose()?,
368 stdout.map(TryFromChildIo::try_from).transpose()?,
369 stderr.map(TryFromChildIo::try_from).transpose()?,
370 )
371 }),
372 ))
373 }
374
375 /// Executes the remote command without waiting for it, returning a handle to it
376 /// instead.
377 ///
378 /// By default, stdin, stdout and stderr are inherited.
379 pub async fn spawn(&mut self) -> Result<Child<S>, Error> {
380 if !self.stdin_set {
381 self.stdin(Stdio::inherit());
382 }
383 if !self.stdout_set {
384 self.stdout(Stdio::inherit());
385 }
386 if !self.stderr_set {
387 self.stderr(Stdio::inherit());
388 }
389
390 self.spawn_impl().await
391 }
392
393 /// Executes the remote command, waiting for it to finish and collecting all of its output.
394 ///
395 /// By default, stdout and stderr are captured (and used to provide the resulting
396 /// output) and stdin is set to `Stdio::null()`.
397 pub async fn output(&mut self) -> Result<process::Output, Error> {
398 if !self.stdin_set {
399 self.stdin(Stdio::null());
400 }
401 if !self.stdout_set {
402 self.stdout(Stdio::piped());
403 }
404 if !self.stderr_set {
405 self.stderr(Stdio::piped());
406 }
407
408 self.spawn_impl().await?.wait_with_output().await
409 }
410
411 /// Executes the remote command, waiting for it to finish and collecting its exit status.
412 ///
413 /// By default, stdin, stdout and stderr are inherited.
414 pub async fn status(&mut self) -> Result<process::ExitStatus, Error> {
415 self.spawn().await?.wait().await
416 }
417}