Skip to main content

dotfiles_actions/exec/
action.rs

1// Copyright (c) 2021-2026 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22//! This module contains the [ExecAction] that executes a command in the shell
23
24use std::path::Path;
25
26use dotfiles_core::action::SKIP_IN_CI_SETTING;
27use dotfiles_core::error::execution_error;
28use dotfiles_core::error::DotfilesError;
29use dotfiles_core::settings::{initialize_settings_object, Setting, Settings};
30use dotfiles_core::{yaml_util, Action};
31use strict_yaml_rust::StrictYaml;
32use subprocess::Exec;
33
34/// Echo the command to run before running it.
35pub const ECHO_SETTING: &str = "echo";
36/// Command to run
37pub const COMMAND_SETTING: &str = "cmd";
38/// Optional description for the command to run
39pub const DESCRIPTION_SETTING: &str = "description";
40
41/// Default settings for the Exec action.
42pub fn default_settings() -> Settings {
43  initialize_settings_object(&[
44    (ECHO_SETTING.to_owned(), Setting::Boolean(false)),
45    (SKIP_IN_CI_SETTING.to_owned(), Setting::Boolean(false)),
46  ])
47}
48
49/// [ExecAction] Installs software using homebrew.
50#[derive(Eq, PartialEq, Debug)]
51pub struct ExecAction {
52  /// Skips this action if it is running in a CI environment.
53  skip_in_ci: bool,
54  /// Command to run
55  command: String,
56  /// Description
57  description: Option<String>,
58  /// Whether to print out the command for clarity.
59  echo: bool,
60}
61
62impl ExecAction {
63  /// Create a new Exec Action that will run from the parent directory of the config file
64  pub fn new(
65    skip_in_ci: bool,
66    command: String,
67    description: Option<String>,
68    echo: bool,
69    current_dir: &Path,
70  ) -> Result<Self, DotfilesError> {
71    let action = ExecAction {
72      skip_in_ci,
73      command: format!(
74        "cd \"{}\" && {}",
75        current_dir.as_os_str().to_str().unwrap(),
76        command
77      ),
78      description,
79      echo,
80    };
81    log::trace!("Creating new {:?}", action);
82    Ok(action)
83  }
84  /// The command to run
85  pub fn command(&self) -> &str {
86    self.command.as_str()
87  }
88
89  /// Whether to print out the command for clarity.
90  pub fn echo(&self) -> bool {
91    self.echo
92  }
93
94  /// Description for the command to run.
95  pub fn description(&self) -> Option<&String> {
96    self.description.as_ref()
97  }
98}
99
100impl Action for ExecAction {
101  fn execute(&self) -> Result<(), DotfilesError> {
102    if let Some(description) = self.description.as_ref() {
103      log::info!("{}", description);
104    }
105    if self.echo {
106      log::info!("Running command: {0}", self.command);
107    }
108    Exec::shell(self.command()).join().map_or_else(
109      |err| {
110        Err(DotfilesError::from(
111          format!(
112            "Couldn't run command `{0}`, failed with error {1}",
113            self.command(),
114            err
115          ),
116          execution_error(Some(err), None),
117        ))
118      },
119      |status| match status.success() {
120        true => Ok(()),
121        false => Err(DotfilesError::from(
122          format!(
123            "Command `{0}` failed with error code {1}",
124            self.command(),
125            status.code().unwrap()
126          ),
127          execution_error(None, Some(status)),
128        )),
129      },
130    )
131  }
132
133  fn skip_in_ci(&self) -> bool {
134    self.skip_in_ci
135  }
136}
137
138/// Static parsing function to build an ExecAction from YAML and settings context
139pub fn parse_action(
140  settings: &Settings,
141  yaml: &StrictYaml,
142  current_dir: &Path,
143) -> Result<ExecAction, DotfilesError> {
144  let defaults = default_settings();
145  ExecAction::new(
146    yaml_util::get_boolean_setting_from_yaml_or_context(
147      SKIP_IN_CI_SETTING,
148      yaml,
149      settings,
150      &defaults,
151    )?,
152    yaml_util::get_string_content_or_keyed_value(yaml, Some(COMMAND_SETTING))?,
153    yaml_util::get_string_setting_from_yaml_or_context(
154      DESCRIPTION_SETTING,
155      yaml,
156      settings,
157      &defaults,
158    )
159    .ok(),
160    yaml_util::get_boolean_setting_from_yaml_or_context(ECHO_SETTING, yaml, settings, &defaults)?,
161    current_dir,
162  )
163}