1use crate::debug;
2use crate::error::Result;
3
4#[derive(Debug)]
5pub enum Mode {
6 CommentToEcho, EchoToComment, }
9
10#[derive(Debug, PartialEq)]
11enum CommentType {
12 Regular(String), NoEcho(String), EscapedHash(String), }
16
17pub fn process_script_content(content: &str, mode: Mode) -> Result<String> {
19 let mut result = String::new();
20
21 result.push_str("#!/usr/bin/env bash\n");
23
24 let mut is_first_line = true;
26 for (i, line) in content.lines().enumerate() {
27 debug!("Processing line {}: '{}'", i, line);
28 if is_first_line && line.starts_with("#!") {
29 debug!("DEBUG: Skipping first-line shebang: {}", line);
31 is_first_line = false;
32 continue;
33 }
34 is_first_line = false;
35
36 let processed_line = match mode {
37 Mode::CommentToEcho => process_comment_to_echo(line),
38 Mode::EchoToComment => process_echo_to_comment(line),
39 };
40
41 debug!("'{}' -> '{}'", line, processed_line);
42
43 result.push_str(&processed_line);
44 result.push('\n');
45 }
46
47 debug!("Finished processing all lines");
48 Ok(result)
49}
50
51fn process_comment_to_echo(line: &str) -> String {
52 if let Some(comment) = extract_comment(line) {
53 match comment {
54 CommentType::Regular(content) => {
55 let indent = get_indent(line);
57 format!("{}echo \"{}\"", indent, escape_for_echo(&content))
58 }
59 CommentType::NoEcho(content) => {
60 let indent = get_indent(line);
62 if content.is_empty() {
63 format!("{}#", indent)
64 } else {
65 format!("{}# {}", indent, content)
66 }
67 }
68 CommentType::EscapedHash(content) => {
69 let indent = get_indent(line);
71 let echo_content = if content.is_empty() {
72 "#".to_string()
73 } else {
74 format!("# {}", content)
75 };
76 format!("{}echo \"{}\"", indent, escape_for_echo(&echo_content))
77 }
78 }
79 } else {
80 line.to_string()
82 }
83}
84
85fn process_echo_to_comment(line: &str) -> String {
86 if let Some(echo_content) = extract_echo(line) {
87 let indent = get_indent(line);
89
90 if let Some(content) = echo_content.strip_prefix("# ") {
92 if content.is_empty() {
94 format!("{}#\\#", indent)
95 } else {
96 format!("{}#\\# {}", indent, content)
97 }
98 } else if echo_content == "#" {
99 format!("{}#\\#", indent)
100 } else if echo_content.is_empty() {
101 format!("{}#", indent)
102 } else {
103 format!("{}# {}", indent, echo_content)
104 }
105 } else {
106 line.to_string()
108 }
109}
110
111fn extract_comment(line: &str) -> Option<CommentType> {
112 let trimmed = line.trim_start();
113
114 if let Some(rest) = trimmed.strip_prefix("#\\# ") {
116 return Some(CommentType::EscapedHash(rest.trim_start().to_string()));
117 } else if trimmed == "#\\#" {
118 return Some(CommentType::EscapedHash(String::new()));
119 }
120
121 if let Some(rest) = trimmed.strip_prefix("## ") {
123 return Some(CommentType::NoEcho(rest.trim_start().to_string()));
124 } else if trimmed == "##" {
125 return Some(CommentType::NoEcho(String::new()));
126 }
127
128 if let Some(rest) = trimmed.strip_prefix("# ") {
130 return Some(CommentType::Regular(rest.trim_start().to_string()));
131 } else if trimmed == "#" {
132 return Some(CommentType::Regular(String::new()));
133 }
134
135 None
136}
137
138fn extract_echo(line: &str) -> Option<String> {
139 let trimmed = line.trim_start();
140
141 if trimmed == "echo" {
143 return Some(String::new());
144 }
145
146 if let Some(rest) = trimmed.strip_prefix("echo ") {
148 let content = rest.trim();
149
150 if (content.starts_with('"') && content.ends_with('"'))
152 || (content.starts_with('\'') && content.ends_with('\''))
153 {
154 let inner = &content[1..content.len() - 1];
155 Some(unescape_from_echo(inner))
156 } else if content.is_empty() {
157 Some(String::new())
158 } else {
159 Some(content.to_string())
161 }
162 } else {
163 None
164 }
165}
166
167fn get_indent(line: &str) -> String {
168 line.chars().take_while(|c| c.is_whitespace()).collect()
169}
170
171fn escape_for_echo(text: &str) -> String {
172 text.replace('\\', "\\\\")
173 .replace('"', "\\\"")
174 .replace('$', "\\$")
175 .replace('`', "\\`")
176}
177
178fn unescape_from_echo(text: &str) -> String {
179 text.replace("\\\"", "\"")
180 .replace("\\$", "$")
181 .replace("\\`", "`")
182 .replace("\\\\", "\\")
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_extract_comment_types() {
191 assert_eq!(
193 extract_comment("# hello world"),
194 Some(CommentType::Regular("hello world".to_string()))
195 );
196 assert_eq!(
197 extract_comment(" # indented comment"),
198 Some(CommentType::Regular("indented comment".to_string()))
199 );
200 assert_eq!(
201 extract_comment("#"),
202 Some(CommentType::Regular(String::new()))
203 );
204
205 assert_eq!(
207 extract_comment("## private comment"),
208 Some(CommentType::NoEcho("private comment".to_string()))
209 );
210 assert_eq!(
211 extract_comment(" ## indented private"),
212 Some(CommentType::NoEcho("indented private".to_string()))
213 );
214 assert_eq!(
215 extract_comment("##"),
216 Some(CommentType::NoEcho(String::new()))
217 );
218
219 assert_eq!(
221 extract_comment("#\\# with hash"),
222 Some(CommentType::EscapedHash("with hash".to_string()))
223 );
224 assert_eq!(
225 extract_comment(" #\\# indented hash"),
226 Some(CommentType::EscapedHash("indented hash".to_string()))
227 );
228 assert_eq!(
229 extract_comment("#\\#"),
230 Some(CommentType::EscapedHash(String::new()))
231 );
232
233 assert_eq!(extract_comment("echo hello"), None);
235 assert_eq!(extract_comment("#!/bin/bash"), None);
236 assert_eq!(extract_comment("#no space"), None);
237 assert_eq!(extract_comment("not # a comment"), None);
238 }
239
240 #[test]
241 fn test_extract_echo() {
242 assert_eq!(
243 extract_echo("echo \"hello world\""),
244 Some("hello world".to_string())
245 );
246 assert_eq!(extract_echo(" echo 'test'"), Some("test".to_string()));
247 assert_eq!(extract_echo("echo"), Some(String::new()));
248 assert_eq!(extract_echo("# comment"), None);
249 assert_eq!(extract_echo("ls -la"), None);
250
251 assert_eq!(extract_echo("echo "), Some(String::new()));
253 assert_eq!(
254 extract_echo("echo unquoted text"),
255 Some("unquoted text".to_string())
256 );
257 assert_eq!(extract_echo("echo \"\""), Some(String::new()));
258 assert_eq!(extract_echo("echo ''"), Some(String::new()));
259 assert_eq!(
260 extract_echo("echo \"with \\\"escaped\\\" quotes\""),
261 Some("with \"escaped\" quotes".to_string())
262 );
263 assert_eq!(
264 extract_echo("echo \"$var and `cmd`\""),
265 Some("$var and `cmd`".to_string())
266 );
267 assert_eq!(
268 extract_echo(" echo \" spaced \" "),
269 Some(" spaced ".to_string())
270 );
271 assert_eq!(extract_echo("echoing"), None);
272 assert_eq!(extract_echo("echo-like"), None);
273 }
274
275 #[test]
276 fn test_get_indent() {
277 assert_eq!(get_indent("# comment"), "");
278 assert_eq!(get_indent(" # indented"), " ");
279 assert_eq!(get_indent("\t# tabbed"), "\t");
280 assert_eq!(get_indent(" \t mixed"), " \t ");
281 assert_eq!(get_indent("no indent"), "");
282 }
283
284 #[test]
285 fn test_escape_unescape_roundtrip() {
286 let test_cases = vec![
287 "simple text",
288 "text with \"quotes\"",
289 "text with $variables",
290 "text with `commands`",
291 "text with \\backslashes",
292 "complex: \"$var\" and `echo test` with \\path",
293 "",
294 ];
295
296 for original in test_cases {
297 let escaped = escape_for_echo(original);
298 let unescaped = unescape_from_echo(&escaped);
299 assert_eq!(original, unescaped, "Failed roundtrip for: {}", original);
300 }
301 }
302
303 #[test]
304 fn test_process_comment_to_echo() {
305 assert_eq!(process_comment_to_echo("# test"), "echo \"test\"");
307 assert_eq!(
308 process_comment_to_echo(" # indented"),
309 " echo \"indented\""
310 );
311 assert_eq!(process_comment_to_echo("#"), "echo \"\"");
312
313 assert_eq!(process_comment_to_echo("## private"), "# private");
315 assert_eq!(
316 process_comment_to_echo(" ## indented private"),
317 " # indented private"
318 );
319 assert_eq!(process_comment_to_echo("##"), "#");
320
321 assert_eq!(
323 process_comment_to_echo("#\\# with hash"),
324 "echo \"# with hash\""
325 );
326 assert_eq!(
327 process_comment_to_echo(" #\\# indented hash"),
328 " echo \"# indented hash\""
329 );
330 assert_eq!(process_comment_to_echo("#\\#"), "echo \"#\"");
331
332 assert_eq!(process_comment_to_echo("not a comment"), "not a comment");
334 assert_eq!(process_comment_to_echo("echo already"), "echo already");
335 }
336
337 #[test]
338 fn test_process_echo_to_comment() {
339 assert_eq!(process_echo_to_comment("echo \"test\""), "# test");
341 assert_eq!(process_echo_to_comment(" echo 'indented'"), " # indented");
342 assert_eq!(process_echo_to_comment("echo"), "#");
343
344 assert_eq!(
346 process_echo_to_comment("echo \"# with hash\""),
347 "#\\# with hash"
348 );
349 assert_eq!(
350 process_echo_to_comment(" echo '# indented hash'"),
351 " #\\# indented hash"
352 );
353 assert_eq!(process_echo_to_comment("echo \"#\""), "#\\#");
354
355 assert_eq!(process_echo_to_comment("not an echo"), "not an echo");
357 assert_eq!(
358 process_echo_to_comment("# already comment"),
359 "# already comment"
360 );
361 }
362
363 #[test]
364 fn test_bidirectional_conversion() {
365 let comment_line = " # Hello world";
367 let echo_line = process_comment_to_echo(comment_line);
368 assert_eq!(echo_line, " echo \"Hello world\"");
369 let back_to_comment = process_echo_to_comment(&echo_line);
370 assert_eq!(back_to_comment, " # Hello world");
371
372 let no_echo_comment = " ## Private note";
374 let processed = process_comment_to_echo(no_echo_comment);
375 assert_eq!(processed, " # Private note");
376
377 let escaped_comment = " #\\# Show hash";
379 let echo_with_hash = process_comment_to_echo(escaped_comment);
380 assert_eq!(echo_with_hash, " echo \"# Show hash\"");
381 let back_to_escaped = process_echo_to_comment(&echo_with_hash);
382 assert_eq!(back_to_escaped, " #\\# Show hash");
383 }
384
385 #[test]
386 fn test_process_script_content() {
387 let input = "#!/usr/bin/env bash\n# test comment\necho existing\n## private note";
388
389 let result = process_script_content(input, Mode::CommentToEcho).unwrap();
390 let expected =
391 "#!/usr/bin/env bash\necho \"test comment\"\necho existing\n# private note\n";
392 assert_eq!(result, expected);
393
394 let result = process_script_content(input, Mode::EchoToComment).unwrap();
395 let expected = "#!/usr/bin/env bash\n# test comment\n# existing\n## private note\n";
396 assert_eq!(result, expected);
397 }
398}