process_tools/
process.rs

1/// Define a private namespace for all its items.
2#[ allow( clippy::std_instead_of_alloc, clippy::std_instead_of_core ) ]
3mod private
4{
5  // use crate::*;
6
7  use std::
8  {
9    fmt::Formatter,
10    path::{ Path, PathBuf },
11    process::{ Command, Stdio },
12  };
13  use std::collections::HashMap;
14  use std::ffi::OsString;
15  use duct::cmd;
16  use error_tools::
17  {
18    untyped::{ Error, Context, format_err },
19    // Result,
20  };
21  use former::Former;
22  use iter_tools::iter::Itertools;
23
24  // ///
25  // /// Executes an external process using the system shell.
26  // ///
27  // /// This function abstracts over the differences between shells on Windows and Unix-based
28  // /// systems, allowing for a unified interface to execute shell commands.
29  // ///
30  // /// # Parameters:
31  // /// - `exec_path`: The command line string to execute in the shell.
32  // /// - `current_path`: The working directory current_path where the command is executed.
33  // ///
34  // /// # Returns:
35  // /// A `Result` containing a `Report` on success, which includes the command's output,
36  // /// or an error if the command fails to execute or complete.
37  // ///
38  // /// # Examples:
39  // /// ```rust
40  // /// use process_tools::process;
41  // ///
42  // /// let report = process::run_with_shell( "echo Hello World", "." ).unwrap();
43  // /// println!( "{}", report.out );
44  // /// ```
45  // ///
46  //
47  // pub fn run_with_shell
48  // (
49  //   exec_path : &str,
50  //   current_path : impl Into< PathBuf >,
51  // )
52  // -> Result< Report, Report >
53  // {
54  //   let current_path = current_path.into();
55  //   let ( program, args ) =
56  //   if cfg!( target_os = "windows" )
57  //   {
58  //     ( "cmd", [ "/C", exec_path ] )
59  //   }
60  //   else
61  //   {
62  //     ( "sh", [ "-c", exec_path ] )
63  //   };
64  //   let options = Run::former()
65  //   .bin_path( program )
66  //   .args( args.into_iter().map( OsString::from ).collect::< Vec< _ > >() )
67  //   .current_path( current_path )
68  //   .form();
69  //   // xxx : qqq : for Petro : implement run for former та для Run
70  //   run( options )
71  // }
72
73  ///
74  /// Executes an external process in a specified directory without using a shell.
75  ///
76  /// # Arguments:
77  /// - `bin_path`: Path to the executable `bin_path`.
78  /// - `args`: Command-line arguments for the `bin_path`.
79  /// - `current_path`: Directory `current_path` to run the `bin_path` in.
80  ///
81  /// # Returns:
82  /// A `Result` containing `Report` on success, detailing execution output,
83  /// or an error message on failure.
84  ///
85  /// # Errors
86  /// Returns an error if the process fails to spawn, complete, or if output
87  /// cannot be decoded as UTF-8.
88  ///
89  /// # Panics
90  /// qqq: doc
91  //
92  // qqq : for Petro : use typed error
93  // qqq : for Petro : write example
94  pub fn run( options : Run ) -> Result< Report, Report >
95  {
96    let bin_path : &Path = options.bin_path.as_ref();
97    let current_path : &Path = options.current_path.as_ref();
98
99    let mut report = Report
100    {
101      command : format!( "{} {}", bin_path.display(), options.args.iter().map( | a | a.to_string_lossy() ).join( " " ) ),
102      current_path : current_path.to_path_buf(),
103      .. Report::default()
104    };
105
106    let mut env: HashMap<String, String> = std::env::vars().collect();
107    env.extend( options.env_variable );
108
109    let output = if options.joining_streams
110    {
111      let output = cmd( bin_path.as_os_str(), &options.args )
112      .dir( current_path )
113      .full_env( env )
114      .stderr_to_stdout()
115      .stdout_capture()
116      .unchecked()
117      .run()
118      .map_err( | e |
119      {
120        report.error = Err( e.into() );
121        Err::< (), () >( () )
122      });
123
124      output
125    }
126    else
127    {
128      let child = Command::new( bin_path )
129      .args( &options.args )
130      .envs( env )
131      .stdout( Stdio::piped() )
132      .stderr( Stdio::piped() )
133      .current_dir( current_path )
134      .spawn()
135      .context( "failed to spawn process" )
136      .map_err( | e |
137      {
138        report.error = Err( e );
139        Err::< (), () >( () )
140      });
141
142      if report.error.is_err()
143      {
144        return Err( report );
145      }
146      let child = child.unwrap();
147
148      child
149      .wait_with_output()
150      .context( "failed to wait on child" )
151      .map_err( | e |
152      {
153        report.error = Err( e );
154        Err::< (), () >( () )
155      })
156    };
157
158    if report.error.is_err()
159    {
160      return Err( report );
161    }
162    let output = output.unwrap();
163
164    let out = String::from_utf8( output.stdout )
165    .context( "Found invalid UTF-8" )
166    .map_err( | e |
167    {
168      report.error = Err( e );
169      Err::< (), () >( () )
170    });
171
172    if out.is_err()
173    {
174      return Err( report );
175    }
176    let out = out.unwrap();
177
178    report.out = out;
179
180    let err = String::from_utf8( output.stderr )
181    .context( "Found invalid UTF-8" )
182    .map_err( | e |
183      {
184        report.error = Err( e );
185        Err::< (), () >( () )
186      });
187
188    if err.is_err()
189    {
190      return Err( report );
191    }
192    let err = err.unwrap();
193
194    report.err = err;
195
196    if output.status.success()
197    {
198      Ok( report )
199    }
200    else
201    {
202      report.error = Err( format_err!( "Process was finished with error code : {}", output.status ) );
203      Err( report )
204    }
205
206  }
207
208  /// Option for `run` function
209  #[ derive( Debug, Former ) ]
210  // #[ debug ]
211  pub struct Run
212  {
213    bin_path : PathBuf,
214    current_path : PathBuf,
215    args : Vec< OsString >,
216    #[ former( default = false ) ]
217    joining_streams : bool,
218    env_variable : HashMap< String, String >,
219  }
220
221  impl RunFormer
222  {
223    pub fn run( self ) -> Result< Report, Report >
224    {
225      run( self.form() )
226    }
227
228    /// Executes an external process using the system shell.
229    ///
230    /// This function abstracts over the differences between shells on Windows and Unix-based
231    /// systems, allowing for a unified interface to execute shell commands.
232    ///
233    /// # Parameters:
234    /// - `exec_path`: The command line string to execute in the shell.
235    ///
236    /// # Returns:
237    /// A `Result` containing a `Report` on success, which includes the command's output,
238    /// or an error if the command fails to execute or complete.
239    pub fn run_with_shell( self, exec_path : &str, ) -> Result< Report, Report >
240    {
241      let ( program, args ) =
242      if cfg!( target_os = "windows" )
243      {
244        ( "cmd", [ "/C", exec_path ] )
245      }
246      else
247      {
248        ( "sh", [ "-c", exec_path ] )
249      };
250      self
251      .args( args.into_iter().map( OsString::from ).collect::< Vec< _ > >() )
252      .bin_path( program )
253      .run()
254    }
255  }
256
257  /// Process command output.
258  #[ derive( Debug, ) ]
259  pub struct Report
260  {
261    /// Command that was executed.
262    pub command : String,
263    /// Path where command was executed.
264    pub current_path : PathBuf,
265    /// Stdout.
266    pub out : String,
267    /// Stderr.
268    pub err : String,
269    /// Error if any
270    pub error : Result< (), Error >
271  }
272
273  impl Clone for Report
274  {
275    fn clone( &self ) -> Self
276    {
277      Self
278      {
279        command : self.command.clone(),
280        current_path : self.current_path.clone(),
281        out : self.out.clone(),
282        err : self.err.clone(),
283        error : self.error.as_ref().map_err( | e | Error::msg( e.to_string() ) ).copied(),
284        // error : self.error.as_ref().map_err( | e | Error::new( e ) ).copied(),
285      }
286    }
287  }
288
289  impl Default for Report
290  {
291    fn default() -> Self
292    {
293      Report
294      {
295        command : String::default(),
296        current_path : PathBuf::new(),
297        out : String::default(),
298        err : String::default(),
299        error : Ok( () ),
300      }
301    }
302  }
303
304  impl core::fmt::Display for Report
305  {
306    fn fmt( &self, f : &mut Formatter< '_ > ) -> core::fmt::Result
307    {
308      // Trim prevents writing unnecessary whitespace or empty lines
309      f.write_fmt( format_args!( "> {}\n", self.command ) )?;
310      f.write_fmt( format_args!( "  @ {}\n\n", self.current_path.display() ) )?;
311
312      if !self.out.trim().is_empty()
313      {
314        f.write_fmt( format_args!( "  {}\n", self.out.replace( '\n', "\n  " ) ) )?;
315      }
316      if !self.err.trim().is_empty()
317      {
318        f.write_fmt( format_args!( "  {}\n", self.err.replace( '\n', "\n  " ) ) )?;
319      }
320
321      Ok( () )
322    }
323  }
324
325}
326
327crate::mod_interface!
328{
329  // own use run_with_shell;
330  own use run;
331  own use Run;
332  own use Report;
333}