docker_wrapper/
command.rs

1//! Command trait architecture for extensible Docker command implementations.
2//!
3//! This module provides a base trait that all Docker commands implement,
4//! allowing for both structured high-level APIs and escape hatches for
5//! any unimplemented options via raw arguments.
6
7use crate::error::{Error, Result};
8use crate::platform::PlatformInfo;
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::ffi::OsStr;
12use std::path::PathBuf;
13use std::process::Stdio;
14use tokio::process::Command as TokioCommand;
15
16// Re-export all command modules
17pub mod attach;
18pub mod bake;
19pub mod build;
20pub mod builder;
21pub mod commit;
22pub mod compose_attach;
23pub mod compose_build;
24pub mod compose_create;
25pub mod compose_down;
26pub mod compose_exec;
27pub mod compose_kill;
28pub mod compose_logs;
29pub mod compose_ls;
30pub mod compose_pause;
31pub mod compose_ps;
32pub mod compose_restart;
33pub mod compose_rm;
34pub mod compose_run;
35pub mod compose_start;
36pub mod compose_stop;
37pub mod compose_unpause;
38pub mod compose_up;
39pub mod container_prune;
40pub mod cp;
41pub mod create;
42pub mod diff;
43pub mod events;
44pub mod exec;
45pub mod export;
46pub mod history;
47pub mod image_prune;
48pub mod images;
49pub mod import;
50pub mod info;
51pub mod inspect;
52pub mod kill;
53pub mod load;
54pub mod login;
55pub mod logout;
56pub mod logs;
57pub mod network;
58pub mod pause;
59pub mod port;
60pub mod ps;
61pub mod pull;
62pub mod push;
63pub mod rename;
64pub mod restart;
65pub mod rm;
66pub mod rmi;
67pub mod run;
68pub mod save;
69pub mod search;
70pub mod start;
71pub mod stats;
72pub mod stop;
73pub mod system;
74pub mod tag;
75pub mod top;
76pub mod unpause;
77pub mod update;
78pub mod version;
79pub mod volume;
80pub mod wait;
81
82/// Unified trait for all Docker commands (both regular and compose)
83#[async_trait]
84pub trait DockerCommand {
85    /// The output type this command produces
86    type Output;
87
88    /// Get the command executor for extensibility
89    fn get_executor(&self) -> &CommandExecutor;
90
91    /// Get mutable command executor for extensibility
92    fn get_executor_mut(&mut self) -> &mut CommandExecutor;
93
94    /// Build the complete command arguments including subcommands
95    fn build_command_args(&self) -> Vec<String>;
96
97    /// Execute the command and return the typed output
98    async fn execute(&self) -> Result<Self::Output>;
99
100    /// Helper method to execute the command with proper error handling
101    async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
102        let executor = self.get_executor();
103
104        // For compose commands, we need to handle "docker compose <subcommand>"
105        // For regular commands, we handle "docker <command>"
106        if command_args.first() == Some(&"compose".to_string()) {
107            // This is a compose command - args are already formatted correctly
108            executor.execute_command("docker", command_args).await
109        } else {
110            // Regular docker command - first arg is the command name
111            let command_name = command_args
112                .first()
113                .unwrap_or(&"docker".to_string())
114                .clone();
115            let remaining_args = command_args.iter().skip(1).cloned().collect();
116            executor
117                .execute_command(&command_name, remaining_args)
118                .await
119        }
120    }
121
122    /// Add a raw argument to the command (escape hatch)
123    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
124        self.get_executor_mut().add_arg(arg);
125        self
126    }
127
128    /// Add multiple raw arguments to the command (escape hatch)
129    fn args<I, S>(&mut self, args: I) -> &mut Self
130    where
131        I: IntoIterator<Item = S>,
132        S: AsRef<OsStr>,
133    {
134        self.get_executor_mut().add_args(args);
135        self
136    }
137
138    /// Add a flag option (e.g., --detach, --rm)
139    fn flag(&mut self, flag: &str) -> &mut Self {
140        self.get_executor_mut().add_flag(flag);
141        self
142    }
143
144    /// Add a key-value option (e.g., --name value, --env key=value)
145    fn option(&mut self, key: &str, value: &str) -> &mut Self {
146        self.get_executor_mut().add_option(key, value);
147        self
148    }
149}
150
151/// Base configuration for all compose commands
152#[derive(Debug, Clone, Default)]
153pub struct ComposeConfig {
154    /// Compose file paths (-f, --file)
155    pub files: Vec<PathBuf>,
156    /// Project name (-p, --project-name)
157    pub project_name: Option<String>,
158    /// Project directory (--project-directory)
159    pub project_directory: Option<PathBuf>,
160    /// Profiles to enable (--profile)
161    pub profiles: Vec<String>,
162    /// Environment file (--env-file)
163    pub env_file: Option<PathBuf>,
164    /// Run in compatibility mode
165    pub compatibility: bool,
166    /// Execute in dry run mode
167    pub dry_run: bool,
168    /// Progress output type
169    pub progress: Option<ProgressType>,
170    /// ANSI control characters
171    pub ansi: Option<AnsiMode>,
172    /// Max parallelism (-1 for unlimited)
173    pub parallel: Option<i32>,
174}
175
176/// Progress output type for compose commands
177#[derive(Debug, Clone, Copy)]
178pub enum ProgressType {
179    /// Auto-detect
180    Auto,
181    /// TTY output
182    Tty,
183    /// Plain text output
184    Plain,
185    /// JSON output
186    Json,
187    /// Quiet mode
188    Quiet,
189}
190
191impl std::fmt::Display for ProgressType {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Self::Auto => write!(f, "auto"),
195            Self::Tty => write!(f, "tty"),
196            Self::Plain => write!(f, "plain"),
197            Self::Json => write!(f, "json"),
198            Self::Quiet => write!(f, "quiet"),
199        }
200    }
201}
202
203/// ANSI control character mode
204#[derive(Debug, Clone, Copy)]
205pub enum AnsiMode {
206    /// Never print ANSI
207    Never,
208    /// Always print ANSI
209    Always,
210    /// Auto-detect
211    Auto,
212}
213
214impl std::fmt::Display for AnsiMode {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        match self {
217            Self::Never => write!(f, "never"),
218            Self::Always => write!(f, "always"),
219            Self::Auto => write!(f, "auto"),
220        }
221    }
222}
223
224impl ComposeConfig {
225    /// Create a new compose configuration
226    #[must_use]
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Add a compose file
232    #[must_use]
233    pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
234        self.files.push(path.into());
235        self
236    }
237
238    /// Set project name
239    #[must_use]
240    pub fn project_name(mut self, name: impl Into<String>) -> Self {
241        self.project_name = Some(name.into());
242        self
243    }
244
245    /// Set project directory
246    #[must_use]
247    pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
248        self.project_directory = Some(dir.into());
249        self
250    }
251
252    /// Add a profile
253    #[must_use]
254    pub fn profile(mut self, profile: impl Into<String>) -> Self {
255        self.profiles.push(profile.into());
256        self
257    }
258
259    /// Set environment file
260    #[must_use]
261    pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
262        self.env_file = Some(path.into());
263        self
264    }
265
266    /// Enable compatibility mode
267    #[must_use]
268    pub fn compatibility(mut self) -> Self {
269        self.compatibility = true;
270        self
271    }
272
273    /// Enable dry run mode
274    #[must_use]
275    pub fn dry_run(mut self) -> Self {
276        self.dry_run = true;
277        self
278    }
279
280    /// Set progress output type
281    #[must_use]
282    pub fn progress(mut self, progress: ProgressType) -> Self {
283        self.progress = Some(progress);
284        self
285    }
286
287    /// Set ANSI mode
288    #[must_use]
289    pub fn ansi(mut self, ansi: AnsiMode) -> Self {
290        self.ansi = Some(ansi);
291        self
292    }
293
294    /// Set max parallelism
295    #[must_use]
296    pub fn parallel(mut self, parallel: i32) -> Self {
297        self.parallel = Some(parallel);
298        self
299    }
300
301    /// Build global compose arguments
302    #[must_use]
303    pub fn build_global_args(&self) -> Vec<String> {
304        let mut args = Vec::new();
305
306        // Add compose files
307        for file in &self.files {
308            args.push("--file".to_string());
309            args.push(file.to_string_lossy().to_string());
310        }
311
312        // Add project name
313        if let Some(ref name) = self.project_name {
314            args.push("--project-name".to_string());
315            args.push(name.clone());
316        }
317
318        // Add project directory
319        if let Some(ref dir) = self.project_directory {
320            args.push("--project-directory".to_string());
321            args.push(dir.to_string_lossy().to_string());
322        }
323
324        // Add profiles
325        for profile in &self.profiles {
326            args.push("--profile".to_string());
327            args.push(profile.clone());
328        }
329
330        // Add environment file
331        if let Some(ref env_file) = self.env_file {
332            args.push("--env-file".to_string());
333            args.push(env_file.to_string_lossy().to_string());
334        }
335
336        // Add flags
337        if self.compatibility {
338            args.push("--compatibility".to_string());
339        }
340
341        if self.dry_run {
342            args.push("--dry-run".to_string());
343        }
344
345        // Add progress type
346        if let Some(progress) = self.progress {
347            args.push("--progress".to_string());
348            args.push(progress.to_string());
349        }
350
351        // Add ANSI mode
352        if let Some(ansi) = self.ansi {
353            args.push("--ansi".to_string());
354            args.push(ansi.to_string());
355        }
356
357        // Add parallel limit
358        if let Some(parallel) = self.parallel {
359            args.push("--parallel".to_string());
360            args.push(parallel.to_string());
361        }
362
363        args
364    }
365}
366
367/// Extended trait for Docker Compose commands
368pub trait ComposeCommand: DockerCommand {
369    /// Get the compose configuration
370    fn get_config(&self) -> &ComposeConfig;
371
372    /// Get mutable compose configuration for builder pattern
373    fn get_config_mut(&mut self) -> &mut ComposeConfig;
374
375    /// Get the compose subcommand name (e.g., "up", "down", "ps")
376    fn subcommand(&self) -> &'static str;
377
378    /// Build command-specific arguments (without global compose args)
379    fn build_subcommand_args(&self) -> Vec<String>;
380
381    /// Build complete command arguments including "compose" and global args\
382    /// (This provides the implementation for `DockerCommandV2::build_command_args`)
383    fn build_command_args(&self) -> Vec<String> {
384        let mut args = vec!["compose".to_string()];
385
386        // Add global compose arguments
387        args.extend(self.get_config().build_global_args());
388
389        // Add the subcommand
390        args.push(self.subcommand().to_string());
391
392        // Add command-specific arguments
393        args.extend(self.build_subcommand_args());
394
395        // Add raw arguments from executor
396        args.extend(self.get_executor().raw_args.clone());
397
398        args
399    }
400
401    /// Helper builder methods for common compose config options
402    #[must_use]
403    fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
404    where
405        Self: Sized,
406    {
407        self.get_config_mut().files.push(file.into());
408        self
409    }
410
411    /// Set project name for compose command
412    #[must_use]
413    fn project_name(mut self, name: impl Into<String>) -> Self
414    where
415        Self: Sized,
416    {
417        self.get_config_mut().project_name = Some(name.into());
418        self
419    }
420}
421
422/// Common functionality for executing Docker commands
423#[derive(Debug, Clone)]
424pub struct CommandExecutor {
425    /// Additional raw arguments added via escape hatch
426    pub raw_args: Vec<String>,
427    /// Platform information for runtime abstraction
428    pub platform_info: Option<PlatformInfo>,
429}
430
431impl CommandExecutor {
432    /// Create a new command executor
433    #[must_use]
434    pub fn new() -> Self {
435        Self {
436            raw_args: Vec::new(),
437            platform_info: None,
438        }
439    }
440
441    /// Create a new command executor with platform detection
442    ///
443    /// # Errors
444    ///
445    /// Returns an error if platform detection fails
446    pub fn with_platform() -> Result<Self> {
447        let platform_info = PlatformInfo::detect()?;
448        Ok(Self {
449            raw_args: Vec::new(),
450            platform_info: Some(platform_info),
451        })
452    }
453
454    /// Set the platform information
455    #[must_use]
456    pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
457        self.platform_info = Some(platform_info);
458        self
459    }
460
461    /// Get the runtime command to use
462    fn get_runtime_command(&self) -> String {
463        if let Some(ref platform_info) = self.platform_info {
464            platform_info.runtime.command().to_string()
465        } else {
466            "docker".to_string()
467        }
468    }
469
470    /// Execute a Docker command with the given arguments
471    ///
472    /// # Errors
473    /// Returns an error if the Docker command fails to execute or returns a non-zero exit code
474    pub async fn execute_command(
475        &self,
476        command_name: &str,
477        args: Vec<String>,
478    ) -> Result<CommandOutput> {
479        // Prepend raw args (they should come before command-specific args)
480        let mut all_args = self.raw_args.clone();
481        all_args.extend(args);
482
483        // Insert the command name at the beginning
484        all_args.insert(0, command_name.to_string());
485
486        let runtime_command = self.get_runtime_command();
487        let mut command = TokioCommand::new(&runtime_command);
488
489        // Set environment variables from platform info
490        if let Some(ref platform_info) = self.platform_info {
491            for (key, value) in platform_info.environment_vars() {
492                command.env(key, value);
493            }
494        }
495
496        let output = command
497            .args(&all_args)
498            .stdout(Stdio::piped())
499            .stderr(Stdio::piped())
500            .output()
501            .await
502            .map_err(|e| {
503                Error::custom(format!(
504                    "Failed to execute {runtime_command} {command_name}: {e}"
505                ))
506            })?;
507
508        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
509        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
510        let success = output.status.success();
511        let exit_code = output.status.code().unwrap_or(-1);
512
513        if !success {
514            return Err(Error::command_failed(
515                format!("{} {}", runtime_command, all_args.join(" ")),
516                exit_code,
517                stdout,
518                stderr,
519            ));
520        }
521
522        Ok(CommandOutput {
523            stdout,
524            stderr,
525            exit_code,
526            success,
527        })
528    }
529
530    /// Add a raw argument
531    pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
532        self.raw_args
533            .push(arg.as_ref().to_string_lossy().to_string());
534    }
535
536    /// Add multiple raw arguments
537    pub fn add_args<I, S>(&mut self, args: I)
538    where
539        I: IntoIterator<Item = S>,
540        S: AsRef<OsStr>,
541    {
542        for arg in args {
543            self.add_arg(arg);
544        }
545    }
546
547    /// Add a flag option
548    pub fn add_flag(&mut self, flag: &str) {
549        let flag_arg = if flag.starts_with('-') {
550            flag.to_string()
551        } else if flag.len() == 1 {
552            format!("-{flag}")
553        } else {
554            format!("--{flag}")
555        };
556        self.raw_args.push(flag_arg);
557    }
558
559    /// Add a key-value option
560    pub fn add_option(&mut self, key: &str, value: &str) {
561        let key_arg = if key.starts_with('-') {
562            key.to_string()
563        } else if key.len() == 1 {
564            format!("-{key}")
565        } else {
566            format!("--{key}")
567        };
568        self.raw_args.push(key_arg);
569        self.raw_args.push(value.to_string());
570    }
571}
572
573impl Default for CommandExecutor {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579/// Output from executing a Docker command
580#[derive(Debug, Clone)]
581pub struct CommandOutput {
582    /// Standard output from the command
583    pub stdout: String,
584    /// Standard error from the command
585    pub stderr: String,
586    /// Exit code
587    pub exit_code: i32,
588    /// Whether the command was successful
589    pub success: bool,
590}
591
592impl CommandOutput {
593    /// Get stdout lines as a vector
594    #[must_use]
595    pub fn stdout_lines(&self) -> Vec<&str> {
596        self.stdout.lines().collect()
597    }
598
599    /// Get stderr lines as a vector
600    #[must_use]
601    pub fn stderr_lines(&self) -> Vec<&str> {
602        self.stderr.lines().collect()
603    }
604
605    /// Check if stdout is empty
606    #[must_use]
607    pub fn stdout_is_empty(&self) -> bool {
608        self.stdout.trim().is_empty()
609    }
610
611    /// Check if stderr is empty
612    #[must_use]
613    pub fn stderr_is_empty(&self) -> bool {
614        self.stderr.trim().is_empty()
615    }
616}
617
618/// Helper for building environment variables
619#[derive(Debug, Clone, Default)]
620pub struct EnvironmentBuilder {
621    vars: HashMap<String, String>,
622}
623
624impl EnvironmentBuilder {
625    /// Create a new environment builder
626    #[must_use]
627    pub fn new() -> Self {
628        Self::default()
629    }
630
631    /// Add an environment variable
632    #[must_use]
633    pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
634        self.vars.insert(key.into(), value.into());
635        self
636    }
637
638    /// Add multiple environment variables from a `HashMap`
639    #[must_use]
640    pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
641        self.vars.extend(vars);
642        self
643    }
644
645    /// Build the environment arguments for Docker
646    #[must_use]
647    pub fn build_args(&self) -> Vec<String> {
648        let mut args = Vec::new();
649        for (key, value) in &self.vars {
650            args.push("--env".to_string());
651            args.push(format!("{key}={value}"));
652        }
653        args
654    }
655
656    /// Get the environment variables as a `HashMap`
657    #[must_use]
658    pub fn as_map(&self) -> &HashMap<String, String> {
659        &self.vars
660    }
661}
662
663/// Helper for building port mappings
664#[derive(Debug, Clone, Default)]
665pub struct PortBuilder {
666    mappings: Vec<PortMapping>,
667}
668
669impl PortBuilder {
670    /// Create a new port builder
671    #[must_use]
672    pub fn new() -> Self {
673        Self::default()
674    }
675
676    /// Add a port mapping
677    #[must_use]
678    pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
679        self.mappings.push(PortMapping {
680            host_port: Some(host_port),
681            container_port,
682            protocol: Protocol::Tcp,
683            host_ip: None,
684        });
685        self
686    }
687
688    /// Add a port mapping with protocol
689    #[must_use]
690    pub fn port_with_protocol(
691        mut self,
692        host_port: u16,
693        container_port: u16,
694        protocol: Protocol,
695    ) -> Self {
696        self.mappings.push(PortMapping {
697            host_port: Some(host_port),
698            container_port,
699            protocol,
700            host_ip: None,
701        });
702        self
703    }
704
705    /// Add a dynamic port mapping (Docker assigns host port)
706    #[must_use]
707    pub fn dynamic_port(mut self, container_port: u16) -> Self {
708        self.mappings.push(PortMapping {
709            host_port: None,
710            container_port,
711            protocol: Protocol::Tcp,
712            host_ip: None,
713        });
714        self
715    }
716
717    /// Build the port arguments for Docker
718    #[must_use]
719    pub fn build_args(&self) -> Vec<String> {
720        let mut args = Vec::new();
721        for mapping in &self.mappings {
722            args.push("--publish".to_string());
723            args.push(mapping.to_string());
724        }
725        args
726    }
727
728    /// Get the port mappings
729    #[must_use]
730    pub fn mappings(&self) -> &[PortMapping] {
731        &self.mappings
732    }
733}
734
735/// Port mapping configuration
736#[derive(Debug, Clone)]
737pub struct PortMapping {
738    /// Host port (None for dynamic allocation)
739    pub host_port: Option<u16>,
740    /// Container port
741    pub container_port: u16,
742    /// Protocol (TCP or UDP)
743    pub protocol: Protocol,
744    /// Host IP to bind to (None for all interfaces)
745    pub host_ip: Option<std::net::IpAddr>,
746}
747
748impl std::fmt::Display for PortMapping {
749    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750        let protocol_suffix = match self.protocol {
751            Protocol::Tcp => "",
752            Protocol::Udp => "/udp",
753        };
754
755        if let Some(host_port) = self.host_port {
756            if let Some(host_ip) = self.host_ip {
757                write!(
758                    f,
759                    "{}:{}:{}{}",
760                    host_ip, host_port, self.container_port, protocol_suffix
761                )
762            } else {
763                write!(
764                    f,
765                    "{}:{}{}",
766                    host_port, self.container_port, protocol_suffix
767                )
768            }
769        } else {
770            write!(f, "{}{}", self.container_port, protocol_suffix)
771        }
772    }
773}
774
775/// Network protocol for port mappings
776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum Protocol {
778    /// TCP protocol
779    Tcp,
780    /// UDP protocol
781    Udp,
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn test_command_executor_args() {
790        let mut executor = CommandExecutor::new();
791        executor.add_arg("test");
792        executor.add_args(vec!["arg1", "arg2"]);
793        executor.add_flag("detach");
794        executor.add_flag("d");
795        executor.add_option("name", "test-container");
796
797        assert_eq!(
798            executor.raw_args,
799            vec![
800                "test",
801                "arg1",
802                "arg2",
803                "--detach",
804                "-d",
805                "--name",
806                "test-container"
807            ]
808        );
809    }
810
811    #[test]
812    fn test_environment_builder() {
813        let env = EnvironmentBuilder::new()
814            .var("KEY1", "value1")
815            .var("KEY2", "value2");
816
817        let args = env.build_args();
818        assert!(args.contains(&"--env".to_string()));
819        assert!(args.contains(&"KEY1=value1".to_string()));
820        assert!(args.contains(&"KEY2=value2".to_string()));
821    }
822
823    #[test]
824    fn test_port_builder() {
825        let ports = PortBuilder::new()
826            .port(8080, 80)
827            .dynamic_port(443)
828            .port_with_protocol(8081, 81, Protocol::Udp);
829
830        let args = ports.build_args();
831        assert!(args.contains(&"--publish".to_string()));
832        assert!(args.contains(&"8080:80".to_string()));
833        assert!(args.contains(&"443".to_string()));
834        assert!(args.contains(&"8081:81/udp".to_string()));
835    }
836
837    #[test]
838    fn test_port_mapping_display() {
839        let tcp_mapping = PortMapping {
840            host_port: Some(8080),
841            container_port: 80,
842            protocol: Protocol::Tcp,
843            host_ip: None,
844        };
845        assert_eq!(tcp_mapping.to_string(), "8080:80");
846
847        let udp_mapping = PortMapping {
848            host_port: Some(8081),
849            container_port: 81,
850            protocol: Protocol::Udp,
851            host_ip: None,
852        };
853        assert_eq!(udp_mapping.to_string(), "8081:81/udp");
854
855        let dynamic_mapping = PortMapping {
856            host_port: None,
857            container_port: 443,
858            protocol: Protocol::Tcp,
859            host_ip: None,
860        };
861        assert_eq!(dynamic_mapping.to_string(), "443");
862    }
863
864    #[test]
865    fn test_command_output_helpers() {
866        let output = CommandOutput {
867            stdout: "line1\nline2".to_string(),
868            stderr: "error1\nerror2".to_string(),
869            exit_code: 0,
870            success: true,
871        };
872
873        assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
874        assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
875        assert!(!output.stdout_is_empty());
876        assert!(!output.stderr_is_empty());
877
878        let empty_output = CommandOutput {
879            stdout: "   ".to_string(),
880            stderr: String::new(),
881            exit_code: 0,
882            success: true,
883        };
884
885        assert!(empty_output.stdout_is_empty());
886        assert!(empty_output.stderr_is_empty());
887    }
888}