1 #[derive(Copy, Clone)]
2 pub(crate) struct Shebang<'line> {
3   pub(crate) interpreter: &'line str,
4   pub(crate) argument: Option<&'line str>,
5 }
6 
7 impl<'line> Shebang<'line> {
new(line: &'line str) -> Option<Shebang<'line>>8   pub(crate) fn new(line: &'line str) -> Option<Shebang<'line>> {
9     if !line.starts_with("#!") {
10       return None;
11     }
12 
13     let mut pieces = line[2..]
14       .lines()
15       .next()
16       .unwrap_or("")
17       .trim()
18       .splitn(2, |c| c == ' ' || c == '\t');
19 
20     let interpreter = pieces.next().unwrap_or("");
21     let argument = pieces.next();
22 
23     if interpreter.is_empty() {
24       return None;
25     }
26 
27     Some(Shebang {
28       interpreter,
29       argument,
30     })
31   }
32 
interpreter_filename(&self) -> &str33   fn interpreter_filename(&self) -> &str {
34     self
35       .interpreter
36       .split(|c| matches!(c, '/' | '\\'))
37       .last()
38       .unwrap_or(self.interpreter)
39   }
40 
script_filename(&self, recipe: &str) -> String41   pub(crate) fn script_filename(&self, recipe: &str) -> String {
42     match self.interpreter_filename() {
43       "cmd" | "cmd.exe" => format!("{}.bat", recipe),
44       "powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => format!("{}.ps1", recipe),
45       _ => recipe.to_owned(),
46     }
47   }
48 
include_shebang_line(&self) -> bool49   pub(crate) fn include_shebang_line(&self) -> bool {
50     !matches!(self.interpreter_filename(), "cmd" | "cmd.exe")
51   }
52 }
53 
54 #[cfg(test)]
55 mod tests {
56   use super::Shebang;
57 
58   #[test]
split_shebang()59   fn split_shebang() {
60     fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {
61       let shebang = Shebang::new(text);
62       assert_eq!(
63         shebang.map(|shebang| (shebang.interpreter, shebang.argument)),
64         expected_split
65       );
66     }
67 
68     check("#!    ", None);
69     check("#!", None);
70     check("#!/bin/bash", Some(("/bin/bash", None)));
71     check("#!/bin/bash    ", Some(("/bin/bash", None)));
72     check(
73       "#!/usr/bin/env python",
74       Some(("/usr/bin/env", Some("python"))),
75     );
76     check(
77       "#!/usr/bin/env python   ",
78       Some(("/usr/bin/env", Some("python"))),
79     );
80     check(
81       "#!/usr/bin/env python -x",
82       Some(("/usr/bin/env", Some("python -x"))),
83     );
84     check(
85       "#!/usr/bin/env python   -x",
86       Some(("/usr/bin/env", Some("python   -x"))),
87     );
88     check(
89       "#!/usr/bin/env python \t-x\t",
90       Some(("/usr/bin/env", Some("python \t-x"))),
91     );
92     check("#/usr/bin/env python \t-x\t", None);
93     check("#!  /bin/bash", Some(("/bin/bash", None)));
94     check("#!\t\t/bin/bash    ", Some(("/bin/bash", None)));
95     check(
96       "#!  \t\t/usr/bin/env python",
97       Some(("/usr/bin/env", Some("python"))),
98     );
99     check(
100       "#!  /usr/bin/env python   ",
101       Some(("/usr/bin/env", Some("python"))),
102     );
103     check(
104       "#!  /usr/bin/env python -x",
105       Some(("/usr/bin/env", Some("python -x"))),
106     );
107     check(
108       "#!  /usr/bin/env python   -x",
109       Some(("/usr/bin/env", Some("python   -x"))),
110     );
111     check(
112       "#!  /usr/bin/env python \t-x\t",
113       Some(("/usr/bin/env", Some("python \t-x"))),
114     );
115     check("#  /usr/bin/env python \t-x\t", None);
116   }
117 
118   #[test]
interpreter_filename_with_forward_slash()119   fn interpreter_filename_with_forward_slash() {
120     assert_eq!(
121       Shebang::new("#!/foo/bar/baz")
122         .unwrap()
123         .interpreter_filename(),
124       "baz"
125     );
126   }
127 
128   #[test]
interpreter_filename_with_backslash()129   fn interpreter_filename_with_backslash() {
130     assert_eq!(
131       Shebang::new("#!\\foo\\bar\\baz")
132         .unwrap()
133         .interpreter_filename(),
134       "baz"
135     );
136   }
137 
138   #[test]
powershell_script_filename()139   fn powershell_script_filename() {
140     assert_eq!(
141       Shebang::new("#!powershell").unwrap().script_filename("foo"),
142       "foo.ps1"
143     );
144   }
145 
146   #[test]
pwsh_script_filename()147   fn pwsh_script_filename() {
148     assert_eq!(
149       Shebang::new("#!pwsh").unwrap().script_filename("foo"),
150       "foo.ps1"
151     );
152   }
153 
154   #[test]
powershell_exe_script_filename()155   fn powershell_exe_script_filename() {
156     assert_eq!(
157       Shebang::new("#!powershell.exe")
158         .unwrap()
159         .script_filename("foo"),
160       "foo.ps1"
161     );
162   }
163 
164   #[test]
pwsh_exe_script_filename()165   fn pwsh_exe_script_filename() {
166     assert_eq!(
167       Shebang::new("#!pwsh.exe").unwrap().script_filename("foo"),
168       "foo.ps1"
169     );
170   }
171 
172   #[test]
cmd_script_filename()173   fn cmd_script_filename() {
174     assert_eq!(
175       Shebang::new("#!cmd").unwrap().script_filename("foo"),
176       "foo.bat"
177     );
178   }
179 
180   #[test]
cmd_exe_script_filename()181   fn cmd_exe_script_filename() {
182     assert_eq!(
183       Shebang::new("#!cmd.exe").unwrap().script_filename("foo"),
184       "foo.bat"
185     );
186   }
187 
188   #[test]
plain_script_filename()189   fn plain_script_filename() {
190     assert_eq!(Shebang::new("#!bar").unwrap().script_filename("foo"), "foo");
191   }
192 
193   #[test]
dont_include_shebang_line_cmd()194   fn dont_include_shebang_line_cmd() {
195     assert!(!Shebang::new("#!cmd").unwrap().include_shebang_line());
196   }
197 
198   #[test]
dont_include_shebang_line_cmd_exe()199   fn dont_include_shebang_line_cmd_exe() {
200     assert!(!Shebang::new("#!cmd.exe /C").unwrap().include_shebang_line());
201   }
202 
203   #[test]
include_shebang_line_other()204   fn include_shebang_line_other() {
205     assert!(Shebang::new("#!foo -c").unwrap().include_shebang_line());
206   }
207 }
208