proc_heim/process/model/
script.rs

1use super::{Cmd, CmdOptions, Runnable};
2use std::path::Path;
3
4/// Constant used as a placeholder for a script file path. See [`ScriptRunConfig`] docs.
5pub const SCRIPT_FILE_PATH_PLACEHOLDER: &str = "@FILE_PATH";
6
7/// Enum type representing a scripting language.
8///
9/// `ScriptingLanguage` provides run configuration for 8 most popular scripting languages.
10/// If you want to use other language, see [`ScriptingLanguage::Other`].
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[non_exhaustive]
14pub enum ScriptingLanguage {
15    /// Executes script with `bash` command.
16    #[default]
17    Bash,
18    /// Executes script with `python` command.
19    Python,
20    /// Executes script with `php -f` command.
21    Php,
22    /// Executes script with `node` command.
23    JavaScript,
24    /// Executes script with `perl` command.
25    Perl,
26    /// Executes script with `lua` command.
27    Lua,
28    /// Executes script with `ruby` command.
29    Ruby,
30    /// Executes script with `groovy` command.
31    Groovy,
32    /// Executes script with provided configuration. See [`ScriptRunConfig`] docs.
33    Other(ScriptRunConfig),
34}
35
36impl From<ScriptingLanguage> for ScriptRunConfig {
37    fn from(value: ScriptingLanguage) -> Self {
38        match value {
39            ScriptingLanguage::Bash => {
40                ScriptRunConfig::new("bash", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "sh")
41            }
42            ScriptingLanguage::Python => {
43                ScriptRunConfig::new("python", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "py")
44            }
45            ScriptingLanguage::Php => {
46                ScriptRunConfig::new("php", vec!["-f", SCRIPT_FILE_PATH_PLACEHOLDER], "php")
47            }
48            ScriptingLanguage::JavaScript => {
49                ScriptRunConfig::new("node", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "js")
50            }
51            ScriptingLanguage::Perl => {
52                ScriptRunConfig::new("perl", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "pl")
53            }
54            ScriptingLanguage::Lua => {
55                ScriptRunConfig::new("lua", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "lua")
56            }
57            ScriptingLanguage::Ruby => {
58                ScriptRunConfig::new("ruby", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "rb")
59            }
60            ScriptingLanguage::Groovy => {
61                ScriptRunConfig::new("groovy", vec![SCRIPT_FILE_PATH_PLACEHOLDER], "groovy")
62            }
63            ScriptingLanguage::Other(run_config) => run_config,
64        }
65    }
66}
67
68/// `ScriptRunConfig` allows to define own configuration used to run a script.
69///
70/// It describes command name, its arguments needed to run a script and also
71/// a file extension typical for a given scripting language.
72/// # Examples
73/// Run configuration for PHP language (equivalent to [`ScriptingLanguage::Php`]):
74/// ```
75/// use proc_heim::model::script::ScriptRunConfig;
76/// use proc_heim::model::script::SCRIPT_FILE_PATH_PLACEHOLDER;
77///
78/// ScriptRunConfig::new("php", ["-f", SCRIPT_FILE_PATH_PLACEHOLDER], "php");
79///
80/// ```
81/// [`SCRIPT_FILE_PATH_PLACEHOLDER`] constant is used to mark that in this argument should be a path to a script file.
82/// Before spawning a script, the placeholder will be replaced by proper file path to the script (with extension provided in `file_extension` argument).
83#[derive(Debug, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct ScriptRunConfig {
86    cmd: String,
87    args: Vec<String>,
88    file_extension: String,
89}
90
91impl ScriptRunConfig {
92    /// Creates a new run configuration.
93    pub fn new<C, T, I, F>(cmd: C, args: I, file_extension: F) -> Self
94    where
95        C: Into<String>,
96        T: Into<String>,
97        I: IntoIterator<Item = T>,
98        F: Into<String>,
99    {
100        Self {
101            cmd: cmd.into(),
102            args: args.into_iter().map(Into::into).collect(),
103            file_extension: file_extension.into(),
104        }
105    }
106
107    pub(crate) fn replace_path_placeholder(&mut self, file_path: &str) {
108        self.args = self
109            .args
110            .iter()
111            .map(|arg| {
112                if arg == SCRIPT_FILE_PATH_PLACEHOLDER {
113                    file_path
114                } else {
115                    arg
116                }
117                .to_owned()
118            })
119            .collect();
120    }
121}
122
123/// `Script` represents a single script.
124///
125/// It requires at least to set a scripting language and content. Script's arguments and options are optional.
126/// [`ScriptingLanguage`] defines the language in which the script is implemented.
127/// Currently, library supports 8 most popular scripting languages, but it is possible to support a custom ones via [`ScriptingLanguage::Other`].
128///
129/// `Script` stores its content in a file and then executes [`Cmd`](struct@crate::model::command::Cmd) provided by [`Runnable`](trait@crate::model::Runnable) trait implementation.
130#[derive(Debug, Clone, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct Script {
133    #[cfg_attr(feature = "serde", serde(default))]
134    pub(crate) lang: ScriptingLanguage,
135    pub(crate) content: String,
136    #[cfg_attr(feature = "serde", serde(default))]
137    pub(crate) args: Vec<String>,
138    #[cfg_attr(feature = "serde", serde(default))]
139    pub(crate) options: CmdOptions,
140}
141
142impl Script {
143    /// Creates a new script with given scripting language and content.
144    /// # Examples
145    /// ```
146    /// # use proc_heim::model::script::*;
147    /// Script::new(ScriptingLanguage::Bash, r#"
148    ///     user=$(echo $USER)
149    ///     echo "Hello $user"
150    /// "#);
151    /// ```
152    pub fn new<S>(lang: ScriptingLanguage, content: S) -> Self
153    where
154        S: Into<String>,
155    {
156        Self {
157            lang,
158            content: content.into(),
159            args: Vec::new(),
160            options: CmdOptions::default(),
161        }
162    }
163
164    /// Creates a new script with given scripting language, content and arguments.
165    /// # Examples
166    /// ```
167    /// # use proc_heim::model::script::*;
168    /// Script::with_args(ScriptingLanguage::Bash, "echo $@ | cut -d ' ' -f2", ["arg1", "arg2"]);
169    /// ```
170    pub fn with_args<S, T, I>(lang: ScriptingLanguage, content: S, args: I) -> Self
171    where
172        S: Into<String>,
173        T: Into<String>,
174        I: IntoIterator<Item = T>,
175    {
176        Self {
177            lang,
178            content: content.into(),
179            args: args.into_iter().map(Into::into).collect(),
180            options: CmdOptions::default(),
181        }
182    }
183
184    /// Creates a new script with given scripting language, content and options.
185    /// # Examples
186    /// ```
187    /// # use proc_heim::model::script::*;
188    /// # use proc_heim::model::command::*;
189    /// let content = r#"
190    ///     for dir in "$(ls -d */)"; do
191    ///        echo "$dir"
192    ///     done
193    ///"#;
194    /// let options = CmdOptions::with_logging(LoggingType::StdoutOnly);
195    /// Script::with_options(ScriptingLanguage::Bash, content, options);
196    /// ```
197    pub fn with_options<S>(lang: ScriptingLanguage, content: S, options: CmdOptions) -> Self
198    where
199        S: Into<String>,
200    {
201        Self {
202            lang,
203            content: content.into(),
204            args: Vec::new(),
205            options,
206        }
207    }
208
209    /// Creates a new script with given scripting language, content, arguments and options.
210    /// # Examples
211    /// ```
212    /// # use proc_heim::model::script::*;
213    /// # use proc_heim::model::command::*;
214    /// let content = r#"
215    ///     base_dir="$1"
216    ///     for dir in "$(ls -d $base_dir/*/)"; do
217    ///         echo "$dir"
218    ///     done
219    /// "#;
220    /// let args = vec!["/some/path"];
221    /// let options = CmdOptions::with_logging(LoggingType::StdoutOnly);
222    /// Script::with_args_and_options(ScriptingLanguage::Bash, content, args, options);
223    /// ```
224    pub fn with_args_and_options<S, T, I>(
225        lang: ScriptingLanguage,
226        content: S,
227        args: I,
228        options: CmdOptions,
229    ) -> Self
230    where
231        S: Into<String>,
232        T: Into<String>,
233        I: IntoIterator<Item = T>,
234    {
235        Self {
236            lang,
237            content: content.into(),
238            args: args.into_iter().map(Into::into).collect(),
239            options,
240        }
241    }
242
243    /// Set a script arguments.
244    /// # Examples
245    /// ```
246    /// # use proc_heim::model::script::*;
247    /// let mut script = Script::new(ScriptingLanguage::Bash, "echo $@ | cut -d ' ' -f2");
248    /// script.set_args(["arg1", "arg2"]);
249    /// ```
250    pub fn set_args<S, I>(&mut self, args: I)
251    where
252        S: Into<String>,
253        I: IntoIterator<Item = S>,
254    {
255        self.args = args.into_iter().map(Into::into).collect();
256    }
257
258    /// Set a script options.
259    /// # Examples
260    /// ```
261    /// # use proc_heim::model::script::*;
262    /// # use proc_heim::model::command::*;
263    /// let mut script = Script::new(ScriptingLanguage::Bash, "echo $@ | cut -d ' ' -f2");
264    /// script.set_options(CmdOptions::with_standard_io_messaging());
265    /// ```
266    pub fn set_options(&mut self, options: CmdOptions) {
267        self.options = options;
268    }
269
270    /// Add a new argument to the end of argument list.
271    /// If arguments was not specified during `Script` creation, it will create new argument list with given argument.
272    /// # Examples
273    /// ```
274    /// # use proc_heim::model::script::*;
275    /// # use proc_heim::model::command::*;
276    /// let mut script = Script::new(ScriptingLanguage::Bash, "echo $@ | cut -d ' ' -f2");
277    /// script.add_arg("arg1");
278    /// script.add_arg("arg2");
279    /// ```
280    pub fn add_arg<S>(&mut self, arg: S)
281    where
282        S: Into<String>,
283    {
284        self.args.push(arg.into());
285    }
286
287    /// Get script language.
288    pub fn language(&self) -> &ScriptingLanguage {
289        &self.lang
290    }
291
292    /// Get script content.
293    pub fn content(&self) -> &str {
294        &self.content
295    }
296
297    /// Get script arguments.
298    pub fn args(&self) -> &[String] {
299        &self.args
300    }
301
302    /// Get script options.
303    pub fn options(&self) -> &CmdOptions {
304        &self.options
305    }
306
307    /// Update script options via mutable reference.
308    /// # Examples
309    /// ```
310    /// # use proc_heim::model::command::*;
311    /// # use proc_heim::model::script::*;
312    /// let mut script = Script::new(ScriptingLanguage::Bash, "echo $TEST_ENV_VAR | cut -d ' ' -f2");
313    /// script.options_mut().add_env("TEST_ENV_VAR", "example value");
314    /// ```
315    pub fn options_mut(&mut self) -> &mut CmdOptions {
316        &mut self.options
317    }
318}
319
320impl Runnable for Script {
321    fn bootstrap_cmd(&self, process_dir: &Path) -> Result<Cmd, String> {
322        let mut run_config: ScriptRunConfig = self.lang.clone().into();
323        let file_path = create_script_file(self, &run_config, process_dir)?;
324        run_config.replace_path_placeholder(&file_path);
325
326        run_config.args.extend_from_slice(&self.args);
327
328        let cmd = Cmd {
329            cmd: run_config.cmd,
330            args: run_config.args,
331            options: self.options.clone(),
332        };
333        Ok(cmd)
334    }
335}
336
337fn create_script_file(
338    script: &Script,
339    run_config: &ScriptRunConfig,
340    script_file_dir: &Path,
341) -> Result<String, String> {
342    let file_path = script_file_dir
343        .join("script")
344        .with_extension(&run_config.file_extension);
345    std::fs::write(&file_path, &script.content).map_err(|err| err.to_string())?;
346    file_path
347        .to_str()
348        .ok_or("Script file path cannot be converted to UTF-8 string".to_owned())
349        .map(|v| v.to_owned())
350}