shell2batch/
converter.rs

1//! # converter
2//!
3//! The module which converts the shell script to windows batch script.
4//!
5
6#[cfg(test)]
7#[path = "./converter_test.rs"]
8mod converter_test;
9
10use regex::Regex;
11
12static SHELL2BATCH_PREFIX: &str = "# shell2batch:";
13
14fn replace_flags(arguments: &str, flags_mappings: Vec<(&str, &str)>) -> String {
15    let mut windows_arguments = arguments.to_string();
16
17    for flags in flags_mappings {
18        let (shell_flag, windows_flag) = flags;
19
20        windows_arguments = match Regex::new(shell_flag) {
21            Ok(shell_regex) => {
22                let str_value = &shell_regex.replace_all(&windows_arguments, windows_flag);
23                str_value.to_string()
24            }
25            Err(_) => windows_arguments,
26        };
27    }
28
29    windows_arguments
30}
31
32fn convert_var<'a>(value: &'a str, buffer: &mut Vec<&'a str>) {
33    // Batch file vars have one of two forms: `%NAME%` (corresponding to regular variables),
34    // or `%n` if `n` is a digit in the range 0 to 9 or an `*` (corresponding to input params).
35    match value {
36        "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
37            buffer.push("%");
38            buffer.push(value);
39        }
40        "@" => buffer.push("%*"),
41        _ => {
42            buffer.push("%");
43            buffer.push(value);
44            buffer.push("%");
45        }
46    }
47}
48
49fn replace_full_vars(arguments: &str) -> String {
50    let mut parts: Vec<&str> = arguments.split("${").collect();
51    let mut buffer = vec![];
52
53    buffer.push(parts.remove(0));
54
55    for part in parts {
56        let (before, after, found) = match part.find("}") {
57            None => (part, "", false),
58            Some(index) => {
59                let values = part.split_at(index);
60
61                (values.0, &values.1[1..values.1.len()], true)
62            }
63        };
64
65        if found {
66            convert_var(before, &mut buffer);
67        } else {
68            buffer.push(before)
69        }
70
71        if after.len() > 0 {
72            buffer.push(after);
73        }
74    }
75
76    buffer.join("").to_string()
77}
78
79fn replace_partial_vars(arguments: &str) -> String {
80    let mut parts: Vec<&str> = arguments.split('$').collect();
81    let mut buffer = vec![];
82
83    buffer.push(parts.remove(0));
84
85    for part in parts {
86        let (before, after) = match part.find(" ") {
87            None => (part, ""),
88            Some(index) => part.split_at(index),
89        };
90
91        convert_var(before, &mut buffer);
92
93        if after.len() > 0 {
94            buffer.push(after);
95        }
96    }
97
98    buffer.join("").to_string()
99}
100
101fn replace_vars(arguments: &str) -> String {
102    let mut updated_arguments = replace_full_vars(arguments);
103    updated_arguments = replace_partial_vars(&updated_arguments);
104
105    updated_arguments
106}
107
108fn add_arguments(arguments: &str, additional_arguments: Vec<String>, pre: bool) -> String {
109    let mut windows_arguments = if pre {
110        "".to_string()
111    } else {
112        arguments.to_string()
113    };
114
115    for additional_argument in additional_arguments {
116        windows_arguments.push_str(&additional_argument);
117    }
118
119    if pre {
120        if arguments.len() > 0 {
121            windows_arguments.push_str(" ");
122        }
123        windows_arguments.push_str(arguments);
124    }
125
126    windows_arguments.trim_start().to_string()
127}
128
129fn convert_line(line: &str) -> String {
130    if line.contains(SHELL2BATCH_PREFIX) {
131        let index = line.find(SHELL2BATCH_PREFIX).unwrap() + SHELL2BATCH_PREFIX.len();
132        let windows_command = line[index..].trim();
133        windows_command.to_string()
134    } else if line.starts_with("#") {
135        let mut windows_command = String::from(line);
136        windows_command.remove(0);
137        windows_command.insert_str(0, "@REM ");
138
139        windows_command
140    } else {
141        // assume first word is the command
142        let (shell_command, mut arguments) = match line.find(" ") {
143            None => (line, "".to_string()),
144            Some(index) => {
145                let (shell_command, arguments_str) = line.split_at(index);
146
147                (shell_command, arguments_str.to_string())
148            }
149        };
150
151        arguments = arguments.trim().to_string();
152
153        let (
154            mut windows_command,
155            flags_mappings,
156            pre_arguments,
157            post_arguments,
158            modify_path_separator,
159        ) = match shell_command {
160            "cp" => {
161                // There is no good `cp` equivalent on windows. There are
162                // two tools we can rely on:
163                //
164                // - xcopy, which is great for directory to directory
165                //   copies.
166                // - copy, which is great for file to file/directory copies.
167                //
168                // We can select which one to use based on the presence of
169                // the -r flag.
170                let win_cmd = match Regex::new("(^|\\s)-[^ ]*[rR]") {
171                    Ok(regex_instance) => {
172                        if regex_instance.is_match(&arguments) {
173                            "xcopy".to_string()
174                        } else {
175                            "copy".to_string()
176                        }
177                    }
178                    Err(_) => "copy".to_string(),
179                };
180
181                let flags_mappings = if win_cmd == "xcopy".to_string() {
182                    vec![("-[rR]", "/E")]
183                } else {
184                    vec![]
185                };
186                (win_cmd, flags_mappings, vec![], vec![], true)
187            }
188            "mv" => ("move".to_string(), vec![], vec![], vec![], true),
189            "ls" => ("dir".to_string(), vec![], vec![], vec![], true),
190            "rm" => {
191                let win_cmd = match Regex::new("-[a-zA-Z]*[rR][a-zA-Z]* ") {
192                    Ok(regex_instance) => {
193                        if regex_instance.is_match(&arguments) {
194                            "rmdir".to_string()
195                        } else {
196                            "del".to_string()
197                        }
198                    }
199                    Err(_) => "del".to_string(),
200                };
201
202                let flags_mappings = if win_cmd == "rmdir".to_string() {
203                    vec![("-([rR][fF]|[fF][rR]) ", "/S /Q "), ("-[rR]+ ", "/S ")]
204                } else {
205                    vec![("-[fF] ", "/Q ")]
206                };
207
208                (win_cmd, flags_mappings, vec![], vec![], true)
209            }
210            "mkdir" => (
211                "mkdir".to_string(),
212                vec![("-[pP]", "")],
213                vec![],
214                vec![],
215                true,
216            ),
217            "clear" => ("cls".to_string(), vec![], vec![], vec![], false),
218            "grep" => ("find".to_string(), vec![], vec![], vec![], false),
219            "pwd" => ("chdir".to_string(), vec![], vec![], vec![], false),
220            "export" => ("set".to_string(), vec![], vec![], vec![], false),
221            "unset" => (
222                "set".to_string(),
223                vec![],
224                vec![],
225                vec!["=".to_string()],
226                false,
227            ),
228            "touch" => {
229                let mut file_arg = arguments.replace("/", "\\").to_string();
230                file_arg.push_str("+,,");
231
232                (
233                    "copy".to_string(),
234                    vec![],
235                    vec!["/B ".to_string(), file_arg.clone()],
236                    vec![],
237                    true,
238                )
239            }
240            "set" => (
241                "@echo".to_string(),
242                vec![("-x", "on"), ("\\+x", "off")],
243                vec![],
244                vec![],
245                false,
246            ),
247            _ => (shell_command.to_string(), vec![], vec![], vec![], false),
248        };
249
250        // modify paths
251        if modify_path_separator {
252            arguments = arguments.replace("/", "\\");
253        }
254        windows_command = windows_command.replace("/", "\\");
255
256        let mut windows_arguments = arguments.to_string();
257
258        // add pre arguments
259        windows_arguments = if pre_arguments.len() > 0 {
260            add_arguments(&windows_arguments, pre_arguments, true)
261        } else {
262            windows_arguments
263        };
264
265        // replace flags
266        windows_arguments = if flags_mappings.len() > 0 {
267            replace_flags(&arguments, flags_mappings)
268        } else {
269            windows_arguments
270        };
271
272        // replace vars
273        windows_arguments = if windows_arguments.len() > 0 {
274            replace_vars(&windows_arguments)
275        } else {
276            windows_arguments
277        };
278        windows_command = replace_vars(&windows_command);
279
280        // add post arguments
281        windows_arguments = if post_arguments.len() > 0 {
282            add_arguments(&windows_arguments, post_arguments, false)
283        } else {
284            windows_arguments
285        };
286
287        if windows_arguments.len() > 0 {
288            windows_command.push_str(" ");
289            windows_command.push_str(&windows_arguments);
290        }
291
292        windows_command
293    }
294}
295
296/// Converts the provided shell script and returns the windows batch script text.
297pub(crate) fn run(script: &str) -> String {
298    let lines: Vec<&str> = script.split('\n').collect();
299    let mut windows_batch = vec![];
300
301    for mut line in lines {
302        line = line.trim();
303        let mut line_string = line.to_string();
304
305        // convert line
306        let converted_line = if line_string.len() == 0 {
307            line_string
308        } else {
309            convert_line(&mut line_string)
310        };
311
312        windows_batch.push(converted_line);
313    }
314
315    windows_batch.join("\n")
316}