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}