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}