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
// Copyright (c) 2021-2022 Miguel Barreto and others
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! This module contains the [ExecAction] that executes a command in the shell

use std::marker::PhantomData;
use std::path::Path;

use dotfiles_core::error::execution_error;
use dotfiles_core::error::DotfilesError;
use dotfiles_core_macros::ConditionalAction;
use subprocess::Exec;
use subprocess::ExitStatus;

use dotfiles_core::Action;

/// [ExecAction] Installs software using homebrew.
#[derive(Eq, PartialEq, Debug, ConditionalAction)]
pub struct ExecAction<'a> {
  /// Skips this action if it is running in a CI environment.
  skip_in_ci: bool,
  /// Command to run
  command: String,
  /// Description
  description: Option<String>,
  /// Whether to print out the command for clarity.
  echo: bool,
  phantom_data: PhantomData<&'a String>,
}

impl<'a> ExecAction<'a> {
  /// Create a new Exec Action that will run from the parent directory of the config file
  pub fn new(
    skip_in_ci: bool,
    command: String,
    description: Option<String>,
    echo: bool,
    current_dir: &Path,
  ) -> Result<Self, DotfilesError> {
    let action = ExecAction {
      skip_in_ci,
      command: format!(
        "cd \"{}\" && {}",
        current_dir.as_os_str().to_str().unwrap(),
        command
      ),
      description,
      echo,
      phantom_data: PhantomData,
    };
    log::trace!("Creating new {:?}", action);
    Ok(action)
  }
  /// The command to run
  pub fn command(&self) -> &str {
    self.command.as_str()
  }

  /// Whether to print out the command for clarity.
  pub fn echo(&self) -> bool {
    self.echo
  }

  /// Description for the command to run.
  pub fn description(&self) -> Option<&String> {
    self.description.as_ref()
  }
}

impl<'a> Action<'a> for ExecAction<'a> {
  fn execute(&self) -> Result<(), DotfilesError> {
    if let Some(description) = self.description.as_ref() {
      log::info!("{}", description);
    }
    if self.echo {
      log::info!("Running command: {0}", self.command);
    }
    Exec::shell(self.command()).join().map_or_else(
      |err| {
        Err(DotfilesError::from(
          format!(
            "Couldn't run command `{0}`, failed with error {1}",
            self.command(),
            err
          ),
          execution_error(Some(err), None),
        ))
      },
      |status| match status {
        ExitStatus::Exited(0) => Ok(()),
        ExitStatus::Exited(code) => Err(DotfilesError::from(
          format!(
            "Command `{0}` failed with error code {1}",
            self.command(),
            code
          ),
          execution_error(None, Some(status)),
        )),
        _ => Err(DotfilesError::from(
          format!(
            "Unexpected error while running command `{0}`",
            self.command()
          ),
          execution_error(None, Some(status)),
        )),
      },
    )
  }
}