Skip to main content

oxilean_codegen/bash_backend/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::HashMap;
6
7use super::types::{
8    BashAnalysisCache, BashArgParser, BashBackend, BashCliOption, BashCondition,
9    BashConstantFoldingHelper, BashDepGraph, BashDominatorTree, BashExpr, BashFunction,
10    BashHereDoc, BashHeredoc, BashJobManager, BashLivenessInfo, BashLogLevel, BashLogger,
11    BashPassConfig, BashPassPhase, BashPassRegistry, BashPassStats, BashScript, BashTemplate,
12    BashTrap, BashVar, BashWorklist,
13};
14
15#[cfg(test)]
16mod tests {
17    use super::*;
18    pub(super) fn backend() -> BashBackend {
19        BashBackend::new()
20    }
21    #[test]
22    pub(super) fn test_bash_var_display_local() {
23        let v = BashVar::Local("count".to_string());
24        assert_eq!(format!("{}", v), "${count}");
25    }
26    #[test]
27    pub(super) fn test_bash_var_display_env() {
28        let v = BashVar::Env("HOME".to_string());
29        assert_eq!(format!("{}", v), "${HOME}");
30    }
31    #[test]
32    pub(super) fn test_bash_var_name() {
33        let v = BashVar::AssocArray("my_map".to_string());
34        assert_eq!(v.name(), "my_map");
35    }
36    #[test]
37    pub(super) fn test_bash_var_is_exported() {
38        assert!(BashVar::Env("X".to_string()).is_exported());
39        assert!(!BashVar::Local("X".to_string()).is_exported());
40        assert!(!BashVar::Global("X".to_string()).is_exported());
41    }
42    #[test]
43    pub(super) fn test_bash_var_is_readonly() {
44        assert!(BashVar::Readonly("X".to_string()).is_readonly());
45        assert!(!BashVar::Local("X".to_string()).is_readonly());
46    }
47    #[test]
48    pub(super) fn test_bash_var_decl_local() {
49        let b = backend();
50        let v = BashVar::Local("x".to_string());
51        assert_eq!(b.emit_var_decl(&v, Some("0")), "local x=0");
52        assert_eq!(b.emit_var_decl(&v, None), "local x");
53    }
54    #[test]
55    pub(super) fn test_bash_var_decl_export() {
56        let b = backend();
57        let v = BashVar::Env("PATH".to_string());
58        let result = b.emit_var_decl(&v, Some("/usr/bin"));
59        assert_eq!(result, "export PATH=/usr/bin");
60    }
61    #[test]
62    pub(super) fn test_bash_var_decl_integer() {
63        let b = backend();
64        let v = BashVar::Integer("n".to_string());
65        assert_eq!(b.emit_var_decl(&v, Some("42")), "declare -i n=42");
66    }
67    #[test]
68    pub(super) fn test_bash_var_decl_assoc_array() {
69        let b = backend();
70        let v = BashVar::AssocArray("opts".to_string());
71        assert_eq!(b.emit_var_decl(&v, None), "declare -A opts");
72    }
73    #[test]
74    pub(super) fn test_bash_var_decl_nameref() {
75        let b = backend();
76        let v = BashVar::NameRef("ref".to_string());
77        assert_eq!(b.emit_var_decl(&v, Some("target")), "declare -n ref=target");
78    }
79    #[test]
80    pub(super) fn test_bash_expr_var() {
81        let b = backend();
82        let expr = BashExpr::Var(BashVar::Local("x".to_string()));
83        assert_eq!(b.emit_expr(&expr), "${x}");
84    }
85    #[test]
86    pub(super) fn test_bash_expr_lit() {
87        let b = backend();
88        let expr = BashExpr::Lit("hello world".to_string());
89        assert_eq!(b.emit_expr(&expr), "'hello world'");
90    }
91    #[test]
92    pub(super) fn test_bash_expr_dquoted() {
93        let b = backend();
94        let expr = BashExpr::DQuoted("hello $name".to_string());
95        assert_eq!(b.emit_expr(&expr), "\"hello $name\"");
96    }
97    #[test]
98    pub(super) fn test_bash_expr_cmd_subst() {
99        let b = backend();
100        let expr = BashExpr::CmdSubst("date +%s".to_string());
101        assert_eq!(b.emit_expr(&expr), "$(date +%s)");
102    }
103    #[test]
104    pub(super) fn test_bash_expr_arith() {
105        let b = backend();
106        let expr = BashExpr::ArithExpr("x + y * 2".to_string());
107        assert_eq!(b.emit_expr(&expr), "$((x + y * 2))");
108    }
109    #[test]
110    pub(super) fn test_bash_expr_array_elem() {
111        let b = backend();
112        let expr = BashExpr::ArrayElem("arr".to_string(), Box::new(BashExpr::Lit("0".to_string())));
113        assert_eq!(b.emit_expr(&expr), "${arr['0']}");
114    }
115    #[test]
116    pub(super) fn test_bash_expr_array_len() {
117        let b = backend();
118        let expr = BashExpr::ArrayLen("items".to_string());
119        assert_eq!(b.emit_expr(&expr), "${#items[@]}");
120    }
121    #[test]
122    pub(super) fn test_bash_expr_array_all() {
123        let b = backend();
124        let expr = BashExpr::ArrayAll("items".to_string());
125        assert_eq!(b.emit_expr(&expr), "\"${items[@]}\"");
126    }
127    #[test]
128    pub(super) fn test_bash_expr_default() {
129        let b = backend();
130        let expr = BashExpr::Default(
131            "NAME".to_string(),
132            Box::new(BashExpr::Lit("world".to_string())),
133        );
134        assert_eq!(b.emit_expr(&expr), "${NAME:-'world'}");
135    }
136    #[test]
137    pub(super) fn test_bash_expr_string_len() {
138        let b = backend();
139        let expr = BashExpr::StringLen("msg".to_string());
140        assert_eq!(b.emit_expr(&expr), "${#msg}");
141    }
142    #[test]
143    pub(super) fn test_bash_expr_strip_prefix() {
144        let b = backend();
145        let expr = BashExpr::StripPrefix("path".to_string(), "*/".to_string());
146        assert_eq!(b.emit_expr(&expr), "${path#*/}");
147    }
148    #[test]
149    pub(super) fn test_bash_expr_strip_suffix() {
150        let b = backend();
151        let expr = BashExpr::StripSuffix("file".to_string(), ".txt".to_string());
152        assert_eq!(b.emit_expr(&expr), "${file%.txt}");
153    }
154    #[test]
155    pub(super) fn test_bash_expr_uppercase() {
156        let b = backend();
157        let expr = BashExpr::UpperCase("word".to_string());
158        assert_eq!(b.emit_expr(&expr), "${word^^}");
159    }
160    #[test]
161    pub(super) fn test_bash_expr_lowercase() {
162        let b = backend();
163        let expr = BashExpr::LowerCase("word".to_string());
164        assert_eq!(b.emit_expr(&expr), "${word,,}");
165    }
166    #[test]
167    pub(super) fn test_bash_expr_special_vars() {
168        let b = backend();
169        assert_eq!(b.emit_expr(&BashExpr::LastStatus), "$?");
170        assert_eq!(b.emit_expr(&BashExpr::ShellPid), "$$");
171        assert_eq!(b.emit_expr(&BashExpr::ScriptName), "$0");
172        assert_eq!(b.emit_expr(&BashExpr::AllArgs), "\"$@\"");
173        assert_eq!(b.emit_expr(&BashExpr::ArgCount), "$#");
174        assert_eq!(b.emit_expr(&BashExpr::Positional(1)), "$1");
175    }
176    #[test]
177    pub(super) fn test_bash_expr_substring() {
178        let b = backend();
179        let e1 = BashExpr::Substring("s".to_string(), 2, None);
180        let e2 = BashExpr::Substring("s".to_string(), 2, Some(5));
181        assert_eq!(b.emit_expr(&e1), "${s:2}");
182        assert_eq!(b.emit_expr(&e2), "${s:2:5}");
183    }
184    #[test]
185    pub(super) fn test_bash_expr_concat() {
186        let b = backend();
187        let expr = BashExpr::Concat(
188            Box::new(BashExpr::DQuoted("hello ".to_string())),
189            Box::new(BashExpr::Var(BashVar::Local("name".to_string()))),
190        );
191        assert_eq!(b.emit_expr(&expr), "\"hello \"${name}");
192    }
193    #[test]
194    pub(super) fn test_bash_condition_file_exists() {
195        let b = backend();
196        let cond = BashCondition::FileExists(BashExpr::DQuoted("/etc/passwd".to_string()));
197        assert_eq!(b.emit_condition(&cond), "[[ -e \"/etc/passwd\" ]]");
198    }
199    #[test]
200    pub(super) fn test_bash_condition_str_eq() {
201        let b = backend();
202        let cond = BashCondition::StrEq(
203            BashExpr::Var(BashVar::Local("a".to_string())),
204            BashExpr::Lit("yes".to_string()),
205        );
206        assert_eq!(b.emit_condition(&cond), "[[ ${a} == 'yes' ]]");
207    }
208    #[test]
209    pub(super) fn test_bash_condition_arith_lt() {
210        let b = backend();
211        let cond = BashCondition::ArithLt("count".to_string(), "10".to_string());
212        assert_eq!(b.emit_condition(&cond), "(( count < 10 ))");
213    }
214    #[test]
215    pub(super) fn test_bash_condition_not() {
216        let b = backend();
217        let inner = BashCondition::IsFile(BashExpr::Lit("config.txt".to_string()));
218        let cond = BashCondition::Not(Box::new(inner));
219        assert_eq!(b.emit_condition(&cond), "! [[ -f 'config.txt' ]]");
220    }
221    #[test]
222    pub(super) fn test_mangle_simple() {
223        let b = backend();
224        assert_eq!(b.mangle_name("my_func"), "my_func");
225        assert_eq!(b.mangle_name("MyFunc"), "MyFunc");
226    }
227    #[test]
228    pub(super) fn test_mangle_dot_separator() {
229        let b = backend();
230        assert_eq!(b.mangle_name("Nat.add"), "Nat__add");
231    }
232    #[test]
233    pub(super) fn test_mangle_colon_separator() {
234        let b = backend();
235        assert_eq!(b.mangle_name("List::map"), "List__map");
236    }
237    #[test]
238    pub(super) fn test_mangle_leading_digit() {
239        let b = backend();
240        let result = b.mangle_name("3foo");
241        assert!(
242            result.starts_with('_'),
243            "expected _ prefix, got: {}",
244            result
245        );
246        assert!(result.contains("foo"));
247    }
248    #[test]
249    pub(super) fn test_mangle_reserved_builtin() {
250        let b = backend();
251        assert_eq!(b.mangle_name("echo"), "echo__ox");
252        assert_eq!(b.mangle_name("read"), "read__ox");
253        assert_eq!(b.mangle_name("local"), "local__ox");
254    }
255    #[test]
256    pub(super) fn test_emit_simple_function() {
257        let b = backend();
258        let func = BashFunction::new("greet", vec!["echo \"Hello, $1!\"".to_string()]);
259        let code = b.emit_function(&func);
260        assert!(code.contains("greet() {"), "got: {}", code);
261        assert!(code.contains("echo \"Hello, $1!\""), "got: {}", code);
262        assert!(code.contains("}\n"), "got: {}", code);
263    }
264    #[test]
265    pub(super) fn test_emit_function_with_locals() {
266        let b = backend();
267        let func = BashFunction::with_locals(
268            "add",
269            vec!["a=$1".to_string(), "b=$2".to_string()],
270            vec!["echo $(( a + b ))".to_string()],
271        );
272        let code = b.emit_function(&func);
273        assert!(code.contains("add() {"));
274        assert!(code.contains("local a=$1"));
275        assert!(code.contains("local b=$2"));
276        assert!(code.contains("echo $(( a + b ))"));
277    }
278    #[test]
279    pub(super) fn test_emit_function_with_description() {
280        let b = backend();
281        let mut func = BashFunction::new("helper", vec!["true".to_string()]);
282        func.description = Some("This is a helper function".to_string());
283        let code = b.emit_function(&func);
284        assert!(code.contains("# This is a helper function"));
285    }
286    #[test]
287    pub(super) fn test_emit_heredoc() {
288        let b = backend();
289        let hd = BashHereDoc::new("EOF", vec!["line1".to_string(), "line2".to_string()]);
290        let code = b.emit_heredoc(&hd);
291        assert!(code.contains("<<EOF"), "got: {}", code);
292        assert!(code.contains("line1"));
293        assert!(code.contains("line2"));
294        assert!(code.contains("EOF"));
295    }
296    #[test]
297    pub(super) fn test_emit_heredoc_no_expand() {
298        let hd = BashHereDoc {
299            delimiter: "EOF".to_string(),
300            strip_tabs: false,
301            no_expand: true,
302            content: vec!["$literal".to_string()],
303        };
304        let b = backend();
305        let code = b.emit_heredoc(&hd);
306        assert!(code.contains("<<'EOF'"), "got: {}", code);
307    }
308    #[test]
309    pub(super) fn test_emit_script_shebang() {
310        let b = backend();
311        let script = BashScript::new();
312        let code = b.emit_script(&script);
313        assert!(code.starts_with("#!/usr/bin/env bash\n"), "got: {}", code);
314    }
315    #[test]
316    pub(super) fn test_emit_script_set_flags() {
317        let b = backend();
318        let script = BashScript::new();
319        let code = b.emit_script(&script);
320        assert!(code.contains("set -euo pipefail"), "got: {}", code);
321    }
322    #[test]
323    pub(super) fn test_emit_script_with_globals() {
324        let b = backend();
325        let mut script = BashScript::new();
326        script
327            .globals
328            .push(("VERSION".to_string(), "1.0.0".to_string()));
329        let code = b.emit_script(&script);
330        assert!(code.contains("readonly VERSION=1.0.0"), "got: {}", code);
331    }
332    #[test]
333    pub(super) fn test_emit_script_with_functions() {
334        let b = backend();
335        let mut script = BashScript::new();
336        script.functions.push(BashFunction::new(
337            "main",
338            vec!["echo 'running'".to_string()],
339        ));
340        let code = b.emit_script(&script);
341        assert!(code.contains("main() {"));
342        assert!(code.contains("echo 'running'"));
343    }
344    #[test]
345    pub(super) fn test_emit_script_with_trap() {
346        let b = backend();
347        let mut script = BashScript::new();
348        script
349            .traps
350            .push(("EXIT".to_string(), "cleanup".to_string()));
351        let code = b.emit_script(&script);
352        assert!(code.contains("trap 'cleanup' EXIT"), "got: {}", code);
353    }
354    #[test]
355    pub(super) fn test_emit_script_full() {
356        let b = backend();
357        let mut script = BashScript::new();
358        script
359            .globals
360            .push(("PROG".to_string(), "oxilean".to_string()));
361        script.functions.push(BashFunction::new(
362            "usage",
363            vec!["echo \"Usage: $PROG [options]\"".to_string()],
364        ));
365        script.main.push("usage".to_string());
366        let code = b.emit_script(&script);
367        assert!(code.contains("#!/usr/bin/env bash"));
368        assert!(code.contains("readonly PROG=oxilean"));
369        assert!(code.contains("usage() {"));
370        assert!(code.contains("usage"));
371    }
372    #[test]
373    pub(super) fn test_emit_array_assign() {
374        let b = backend();
375        let elems = vec![
376            BashExpr::Lit("a".to_string()),
377            BashExpr::Lit("b".to_string()),
378            BashExpr::Lit("c".to_string()),
379        ];
380        let code = b.emit_array_assign("items", &elems);
381        assert_eq!(code, "items=('a' 'b' 'c')");
382    }
383    #[test]
384    pub(super) fn test_emit_assoc_array_assign() {
385        let b = backend();
386        let pairs = vec![
387            ("key1".to_string(), "val1".to_string()),
388            ("key2".to_string(), "val2".to_string()),
389        ];
390        let code = b.emit_assoc_array_assign("opts", &pairs);
391        assert!(code.contains("declare -A opts"), "got: {}", code);
392        assert!(code.contains("opts[key1]=val1"), "got: {}", code);
393        assert!(code.contains("opts[key2]=val2"), "got: {}", code);
394    }
395    #[test]
396    pub(super) fn test_emit_if() {
397        let b = backend();
398        let cond = BashCondition::StrEq(
399            BashExpr::Var(BashVar::Local("x".to_string())),
400            BashExpr::Lit("yes".to_string()),
401        );
402        let code = b.emit_if(&cond, &["echo 'yes'"], Some(&["echo 'no'"]));
403        assert!(
404            code.contains("if [[ ${x} == 'yes' ]]; then"),
405            "got: {}",
406            code
407        );
408        assert!(code.contains("echo 'yes'"), "got: {}", code);
409        assert!(code.contains("else"), "got: {}", code);
410        assert!(code.contains("echo 'no'"), "got: {}", code);
411        assert!(code.contains("fi"), "got: {}", code);
412    }
413    #[test]
414    pub(super) fn test_emit_for_in() {
415        let b = backend();
416        let items = vec![
417            BashExpr::Lit("a".to_string()),
418            BashExpr::Lit("b".to_string()),
419        ];
420        let code = b.emit_for_in("item", &items, &["echo $item"]);
421        assert!(code.contains("for item in 'a' 'b'; do"), "got: {}", code);
422        assert!(code.contains("echo $item"), "got: {}", code);
423        assert!(code.contains("done"), "got: {}", code);
424    }
425    #[test]
426    pub(super) fn test_emit_while() {
427        let b = backend();
428        let cond = BashCondition::ArithLt("i".to_string(), "10".to_string());
429        let code = b.emit_while(&cond, &["echo $i", "(( i++ ))"]);
430        assert!(code.contains("while (( i < 10 )); do"), "got: {}", code);
431        assert!(code.contains("echo $i"), "got: {}", code);
432        assert!(code.contains("done"), "got: {}", code);
433    }
434    #[test]
435    pub(super) fn test_emit_case() {
436        let b = backend();
437        let expr = BashExpr::Var(BashVar::Local("cmd".to_string()));
438        let arms = vec![
439            ("start", vec!["do_start"]),
440            ("stop", vec!["do_stop"]),
441            ("*", vec!["echo 'unknown'"]),
442        ];
443        let code = b.emit_case(&expr, &arms);
444        assert!(code.contains("case ${cmd} in"), "got: {}", code);
445        assert!(code.contains("start)"), "got: {}", code);
446        assert!(code.contains("do_start"), "got: {}", code);
447        assert!(code.contains("esac"), "got: {}", code);
448    }
449    #[test]
450    pub(super) fn test_lenient_script_no_strict() {
451        let b = backend();
452        let script = BashScript::lenient();
453        let code = b.emit_script(&script);
454        assert!(code.starts_with("#!/usr/bin/env bash"));
455        assert!(
456            !code.contains("set -euo pipefail"),
457            "lenient should not have strict mode"
458        );
459    }
460    #[test]
461    pub(super) fn test_compact_backend_indent() {
462        let b = BashBackend::compact();
463        let func = BashFunction::new("f", vec!["echo hi".to_string()]);
464        let code = b.emit_function(&func);
465        assert!(code.contains("  echo hi"), "got: {}", code);
466    }
467}
468/// ANSI terminal color constants for bash scripts.
469#[allow(dead_code)]
470pub mod bash_colors {
471    pub const RESET: &str = "\\033[0m";
472    pub const BOLD: &str = "\\033[1m";
473    pub const DIM: &str = "\\033[2m";
474    pub const ITALIC: &str = "\\033[3m";
475    pub const UNDERLINE: &str = "\\033[4m";
476    pub const BLINK: &str = "\\033[5m";
477    pub const REVERSE: &str = "\\033[7m";
478    pub const HIDDEN: &str = "\\033[8m";
479    pub const STRIKE: &str = "\\033[9m";
480    pub const BLACK: &str = "\\033[30m";
481    pub const RED: &str = "\\033[31m";
482    pub const GREEN: &str = "\\033[32m";
483    pub const YELLOW: &str = "\\033[33m";
484    pub const BLUE: &str = "\\033[34m";
485    pub const MAGENTA: &str = "\\033[35m";
486    pub const CYAN: &str = "\\033[36m";
487    pub const WHITE: &str = "\\033[37m";
488    pub const BRIGHT_BLACK: &str = "\\033[90m";
489    pub const BRIGHT_RED: &str = "\\033[91m";
490    pub const BRIGHT_GREEN: &str = "\\033[92m";
491    pub const BRIGHT_YELLOW: &str = "\\033[93m";
492    pub const BRIGHT_BLUE: &str = "\\033[94m";
493    pub const BRIGHT_MAGENTA: &str = "\\033[95m";
494    pub const BRIGHT_CYAN: &str = "\\033[96m";
495    pub const BRIGHT_WHITE: &str = "\\033[97m";
496    pub const BG_RED: &str = "\\033[41m";
497    pub const BG_GREEN: &str = "\\033[42m";
498    pub const BG_YELLOW: &str = "\\033[43m";
499    pub const BG_BLUE: &str = "\\033[44m";
500    pub const BG_MAGENTA: &str = "\\033[45m";
501    pub const BG_CYAN: &str = "\\033[46m";
502}
503/// Generate bash code to split a string by a delimiter.
504#[allow(dead_code)]
505pub fn emit_bash_split(str_var: &str, delim: &str, arr_var: &str) -> std::string::String {
506    format!(
507        "IFS='{}' read -r -a {} <<< \"${{{}}}\"\n",
508        delim, arr_var, str_var
509    )
510}
511/// Generate bash code to join an array with a delimiter.
512#[allow(dead_code)]
513pub fn emit_bash_join(arr_var: &str, delim: &str, result_var: &str) -> std::string::String {
514    format!(
515        "local IFS='{}'; {}=\"${{{}[*]}}\"\n",
516        delim, result_var, arr_var
517    )
518}
519/// Generate bash code to trim whitespace from a variable.
520#[allow(dead_code)]
521pub fn emit_bash_trim(var: &str, result_var: &str) -> std::string::String {
522    format!(
523        "{var}=\"${{${{{var}}}##*( )}}\"  # trim leading\n{result_var}=\"${{${{{var}}}%%*( )}}\"  # trim trailing\n",
524        var = var, result_var = result_var
525    )
526}
527/// Generate bash code to URL-encode a string.
528#[allow(dead_code)]
529pub fn emit_bash_url_encode(var: &str, result_var: &str) -> std::string::String {
530    format!(
531        "{result}=$(printf '%s' \"${{{var}}}\" | jq -Rr @uri 2>/dev/null || python3 -c \"import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().rstrip()))\" <<< \"${{{var}}}\")\n",
532        var = var, result = result_var
533    )
534}
535/// Generate bash code to check if a command exists.
536#[allow(dead_code)]
537pub fn emit_bash_require_cmd(cmd: &str) -> std::string::String {
538    format!(
539        "command -v {} &>/dev/null || {{ echo \"Error: '{}' not found in PATH\" >&2; exit 1; }}\n",
540        cmd, cmd
541    )
542}
543/// Emits bash code to source a .env or .conf file.
544#[allow(dead_code)]
545pub fn emit_bash_source_env(file_path: &str, required: bool) -> std::string::String {
546    if required {
547        format!(
548            "if [[ -f \"{path}\" ]]; then\n  # shellcheck source=/dev/null\n  source \"{path}\"\nelse\n  echo \"Error: config file not found: {path}\" >&2\n  exit 1\nfi\n",
549            path = file_path
550        )
551    } else {
552        format!(
553            "if [[ -f \"{path}\" ]]; then\n  # shellcheck source=/dev/null\n  source \"{path}\"\nfi\n",
554            path = file_path
555        )
556    }
557}
558/// Emits bash code to read a .env file safely (without sourcing).
559#[allow(dead_code)]
560pub fn emit_bash_read_env(file_path: &str) -> std::string::String {
561    format!(
562        "while IFS='=' read -r _key _val || [[ -n \"$_key\" ]]; do\n  [[ \"$_key\" =~ ^#.*$ || -z \"$_key\" ]] && continue\n  export \"$_key\"=\"$_val\"\ndone < \"{}\"\n",
563        file_path
564    )
565}
566/// Emits bash code to acquire an exclusive lock file.
567#[allow(dead_code)]
568pub fn emit_bash_lock(lock_file: &str, lock_fd_var: &str) -> std::string::String {
569    format!(
570        "exec {fd}<>\"{file}\"\nflock -n ${fd} || {{ echo \"Another instance is running\" >&2; exit 1; }}\n",
571        fd = lock_fd_var, file = lock_file
572    )
573}
574/// Emits bash code to release a lock file.
575#[allow(dead_code)]
576pub fn emit_bash_unlock(lock_fd_var: &str) -> std::string::String {
577    format!("flock -u ${}\n", lock_fd_var)
578}
579/// Emits a bash retry wrapper function.
580#[allow(dead_code)]
581pub fn emit_bash_retry_fn(max_attempts: u8, delay_secs: u8) -> std::string::String {
582    format!(
583        "retry() {{\n  local _attempt=1\n  until \"$@\"; do\n    _attempt=$(( _attempt + 1 ))\n    if [[ $_attempt -gt {max} ]]; then\n      echo \"Command failed after {max} attempts\" >&2\n      return 1\n    fi\n    echo \"Attempt $_attempt of {max} failed, retrying in {delay}s...\" >&2\n    sleep {delay}\n  done\n}}\n",
584        max = max_attempts, delay = delay_secs
585    )
586}
587/// Emits a bash progress bar function.
588#[allow(dead_code)]
589pub fn emit_bash_progress_bar(width: usize) -> std::string::String {
590    format!(
591        "progress_bar() {{\n  local current=$1 total=$2 width={w}\n  local pct=$(( current * 100 / total ))\n  local filled=$(( pct * width / 100 ))\n  local bar; bar=$(printf '%0.s#' $(seq 1 $filled))\n  local empty; empty=$(printf '%0.s-' $(seq 1 $(( width - filled ))))\n  printf \"\\r[%s%s] %d%%\" \"$bar\" \"$empty\" \"$pct\"\n  [[ $current -eq $total ]] && echo\n}}\n",
592        w = width
593    )
594}
595/// Emits bash code to make an HTTP GET request.
596#[allow(dead_code)]
597pub fn emit_bash_http_get(
598    url_var: &str,
599    result_var: &str,
600    _timeout_secs: u8,
601) -> std::string::String {
602    format!(
603        "{result}=$(curl -fsSL --max-time {timeout} \"${{{url}}}\" 2>/dev/null)\nif [[ $? -ne 0 ]]; then\n  echo \"HTTP GET failed for ${{url}}\" >&2\n  exit 1\nfi\n",
604        result = result_var, timeout = _timeout_secs, url = url_var
605    )
606}
607/// Emits bash code to make an HTTP POST request with JSON.
608#[allow(dead_code)]
609pub fn emit_bash_http_post_json(
610    url_var: &str,
611    body_var: &str,
612    result_var: &str,
613    _timeout_secs: u8,
614) -> std::string::String {
615    format!(
616        "{result}=$(curl -fsSL --max-time {timeout} -X POST -H 'Content-Type: application/json' -d \"${{{body}}}\" \"${{{url}}}\" 2>/dev/null)\n",
617        result = result_var, timeout = _timeout_secs, body = body_var, url = url_var
618    )
619}
620/// Emit a C-style for loop.
621#[allow(dead_code)]
622pub fn emit_bash_for_arith(
623    init: &str,
624    cond: &str,
625    incr: &str,
626    body: &[&str],
627    indent: &str,
628) -> std::string::String {
629    let mut out = format!("for (( {}; {}; {} )); do\n", init, cond, incr);
630    for stmt in body {
631        out.push_str(&format!("{}{}\n", indent, stmt));
632    }
633    out.push_str("done\n");
634    out
635}
636/// Emit a bash until loop.
637#[allow(dead_code)]
638pub fn emit_bash_until(cond: &BashCondition, body: &[&str], indent: &str) -> std::string::String {
639    let mut out = format!("until {}; do\n", cond);
640    for stmt in body {
641        out.push_str(&format!("{}{}\n", indent, stmt));
642    }
643    out.push_str("done\n");
644    out
645}
646/// Emit a select menu.
647#[allow(dead_code)]
648pub fn emit_bash_select_menu(
649    var: &str,
650    choices: &[&str],
651    body: &[&str],
652    indent: &str,
653) -> std::string::String {
654    let choices_str: Vec<std::string::String> =
655        choices.iter().map(|c| format!("\"{}\"", c)).collect();
656    let mut out = format!(
657        "PS3=\"Select: \"\nselect {} in {}; do\n",
658        var,
659        choices_str.join(" ")
660    );
661    for stmt in body {
662        out.push_str(&format!("{}{}\n", indent, stmt));
663    }
664    out.push_str("done\n");
665    out
666}
667#[cfg(test)]
668mod bash_extended_tests {
669    use super::*;
670    #[test]
671    pub(super) fn test_heredoc_emit() {
672        let h = BashHeredoc::new("EOF")
673            .line("Hello, World!")
674            .line("Second line");
675        let out = h.emit();
676        assert!(out.contains("<<EOF"), "missing heredoc tag: {}", out);
677        assert!(out.contains("Hello"), "missing content: {}", out);
678    }
679    #[test]
680    pub(super) fn test_heredoc_quoted() {
681        let h = BashHeredoc::new("SCRIPT").quoted().line("$var");
682        let out = h.emit();
683        assert!(out.contains("<<'SCRIPT'"), "missing quoted tag: {}", out);
684    }
685    #[test]
686    pub(super) fn test_trap_emit() {
687        let t = BashTrap::on_exit("cleanup");
688        assert_eq!(t.emit(), "trap 'cleanup' EXIT");
689        let t2 = BashTrap::on_err("handle_error");
690        assert_eq!(t2.emit(), "trap 'handle_error' ERR");
691        let t3 = BashTrap::reset("INT");
692        assert!(t3.emit().contains("INT"), "missing signal: {}", t3.emit());
693    }
694    #[test]
695    pub(super) fn test_log_level_ordering() {
696        assert!(BashLogLevel::Debug < BashLogLevel::Info);
697        assert!(BashLogLevel::Info < BashLogLevel::Warn);
698        assert!(BashLogLevel::Warn < BashLogLevel::Error);
699        assert!(BashLogLevel::Error < BashLogLevel::Fatal);
700        assert!(BashLogLevel::Error.is_stderr());
701        assert!(BashLogLevel::Fatal.is_stderr());
702        assert!(!BashLogLevel::Info.is_stderr());
703    }
704    #[test]
705    pub(super) fn test_logger_emit_framework() {
706        let logger = BashLogger::new()
707            .with_timestamps()
708            .with_color()
709            .with_min_level(BashLogLevel::Debug)
710            .with_log_file("/var/log/app.log");
711        let code = logger.emit_framework();
712        assert!(code.contains("_log()"), "missing _log: {}", code);
713        assert!(code.contains("log_debug"), "missing log_debug: {}", code);
714        assert!(code.contains("log_fatal"), "missing log_fatal: {}", code);
715        assert!(
716            code.contains("/var/log/app.log"),
717            "missing log file: {}",
718            code
719        );
720    }
721    #[test]
722    pub(super) fn test_cli_option_flag() {
723        let opt = BashCliOption::flag("verbose", Some('v'), "verbose", "Enable verbose output");
724        assert!(!opt.has_arg);
725        assert_eq!(opt.default, Some("false".to_string()));
726        assert!(!opt.required);
727    }
728    #[test]
729    pub(super) fn test_arg_parser_emit() {
730        let parser = BashArgParser::new("myapp", "A test application")
731            .add_option(BashCliOption::flag(
732                "verbose",
733                Some('v'),
734                "verbose",
735                "Verbose mode",
736            ))
737            .add_option(
738                BashCliOption::arg(
739                    "output",
740                    Some('o'),
741                    "output",
742                    Some("/tmp/out"),
743                    "Output file",
744                )
745                .required(),
746            )
747            .add_positional("input_file");
748        let usage = parser.emit_usage();
749        assert!(usage.contains("usage()"), "missing usage fn: {}", usage);
750        assert!(usage.contains("--verbose"), "missing verbose: {}", usage);
751        assert!(usage.contains("--output"), "missing output: {}", usage);
752        let parse = parser.emit_parse_block();
753        assert!(
754            parse.contains("VERBOSE=false"),
755            "missing default: {}",
756            parse
757        );
758        assert!(
759            parse.contains("--verbose"),
760            "missing verbose case: {}",
761            parse
762        );
763        assert!(
764            parse.contains("OUTPUT is required"),
765            "missing required check: {}",
766            parse
767        );
768    }
769    #[test]
770    pub(super) fn test_job_manager_emit() {
771        let jm = BashJobManager::new(4).with_pids_var("JOB_PIDS");
772        let code = jm.emit_framework();
773        assert!(code.contains("_MAX_JOBS=4"), "missing max_jobs: {}", code);
774        assert!(code.contains("JOB_PIDS"), "missing pids var: {}", code);
775        assert!(code.contains("run_job()"), "missing run_job: {}", code);
776        assert!(code.contains("_wait_jobs()"), "missing wait_jobs: {}", code);
777    }
778    #[test]
779    pub(super) fn test_template_render() {
780        let tmpl = BashTemplate::new("Hello, {{NAME}}! You are {{AGE}} years old.")
781            .set("NAME", "Alice")
782            .set("AGE", "30");
783        let rendered = tmpl.render();
784        assert_eq!(rendered, "Hello, Alice! You are 30 years old.");
785    }
786    #[test]
787    pub(super) fn test_split_join_emit() {
788        let split = emit_bash_split("INPUT", ":", "PARTS");
789        assert!(split.contains("IFS=':'"), "missing IFS: {}", split);
790        assert!(split.contains("PARTS"), "missing arr: {}", split);
791        let join = emit_bash_join("PARTS", ",", "RESULT");
792        assert!(join.contains("IFS=','"), "missing IFS: {}", join);
793    }
794    #[test]
795    pub(super) fn test_retry_fn_emit() {
796        let code = emit_bash_retry_fn(3, 5);
797        assert!(code.contains("retry()"), "missing fn: {}", code);
798        assert!(code.contains("3"), "missing max: {}", code);
799        assert!(code.contains("sleep 5"), "missing sleep: {}", code);
800    }
801    #[test]
802    pub(super) fn test_progress_bar_emit() {
803        let code = emit_bash_progress_bar(50);
804        assert!(code.contains("progress_bar()"), "missing fn: {}", code);
805        assert!(code.contains("50"), "missing width: {}", code);
806    }
807    #[test]
808    pub(super) fn test_source_env_emit() {
809        let required = emit_bash_source_env("/etc/app/config.conf", true);
810        assert!(required.contains("source"), "missing source: {}", required);
811        assert!(required.contains("exit 1"), "missing exit: {}", required);
812        let optional = emit_bash_source_env("/etc/app/config.conf", false);
813        assert!(optional.contains("source"), "missing source: {}", optional);
814        assert!(
815            !optional.contains("exit 1"),
816            "should not exit: {}",
817            optional
818        );
819    }
820    #[test]
821    pub(super) fn test_lock_unlock_emit() {
822        let lock = emit_bash_lock("/var/run/app.lock", "9");
823        assert!(lock.contains("flock"), "missing flock: {}", lock);
824        let unlock = emit_bash_unlock("9");
825        assert!(unlock.contains("flock -u"), "missing unlock: {}", unlock);
826    }
827    #[test]
828    pub(super) fn test_for_arith_emit() {
829        let code = emit_bash_for_arith("i=0", "i<10", "i++", &["echo $i"], "  ");
830        assert!(
831            code.contains("for (( i=0; i<10; i++ ))"),
832            "missing for arith: {}",
833            code
834        );
835        assert!(code.contains("echo $i"), "missing body: {}", code);
836        assert!(code.contains("done"), "missing done: {}", code);
837    }
838    #[test]
839    pub(super) fn test_bash_colors_module() {
840        assert!(bash_colors::RED.contains("31m"));
841        assert!(bash_colors::GREEN.contains("32m"));
842        assert!(bash_colors::BLUE.contains("34m"));
843        assert!(bash_colors::RESET.contains("0m"));
844        assert!(bash_colors::BOLD.contains("1m"));
845    }
846    #[test]
847    pub(super) fn test_require_cmd_emit() {
848        let code = emit_bash_require_cmd("jq");
849        assert!(
850            code.contains("command -v jq"),
851            "missing command check: {}",
852            code
853        );
854        assert!(code.contains("exit 1"), "missing exit: {}", code);
855    }
856    #[test]
857    pub(super) fn test_http_get_emit() {
858        let code = emit_bash_http_get("URL", "RESPONSE", 30);
859        assert!(code.contains("curl"), "missing curl: {}", code);
860        assert!(code.contains("RESPONSE"), "missing result var: {}", code);
861        assert!(code.contains("30"), "missing timeout: {}", code);
862    }
863}
864#[cfg(test)]
865mod Bash_infra_tests {
866    use super::*;
867    #[test]
868    pub(super) fn test_pass_config() {
869        let config = BashPassConfig::new("test_pass", BashPassPhase::Transformation);
870        assert!(config.enabled);
871        assert!(config.phase.is_modifying());
872        assert_eq!(config.phase.name(), "transformation");
873    }
874    #[test]
875    pub(super) fn test_pass_stats() {
876        let mut stats = BashPassStats::new();
877        stats.record_run(10, 100, 3);
878        stats.record_run(20, 200, 5);
879        assert_eq!(stats.total_runs, 2);
880        assert!((stats.average_changes_per_run() - 15.0).abs() < 0.01);
881        assert!((stats.success_rate() - 1.0).abs() < 0.01);
882        let s = stats.format_summary();
883        assert!(s.contains("Runs: 2/2"));
884    }
885    #[test]
886    pub(super) fn test_pass_registry() {
887        let mut reg = BashPassRegistry::new();
888        reg.register(BashPassConfig::new("pass_a", BashPassPhase::Analysis));
889        reg.register(BashPassConfig::new("pass_b", BashPassPhase::Transformation).disabled());
890        assert_eq!(reg.total_passes(), 2);
891        assert_eq!(reg.enabled_count(), 1);
892        reg.update_stats("pass_a", 5, 50, 2);
893        let stats = reg.get_stats("pass_a").expect("stats should exist");
894        assert_eq!(stats.total_changes, 5);
895    }
896    #[test]
897    pub(super) fn test_analysis_cache() {
898        let mut cache = BashAnalysisCache::new(10);
899        cache.insert("key1".to_string(), vec![1, 2, 3]);
900        assert!(cache.get("key1").is_some());
901        assert!(cache.get("key2").is_none());
902        assert!((cache.hit_rate() - 0.5).abs() < 0.01);
903        cache.invalidate("key1");
904        assert!(!cache.entries["key1"].valid);
905        assert_eq!(cache.size(), 1);
906    }
907    #[test]
908    pub(super) fn test_worklist() {
909        let mut wl = BashWorklist::new();
910        assert!(wl.push(1));
911        assert!(wl.push(2));
912        assert!(!wl.push(1));
913        assert_eq!(wl.len(), 2);
914        assert_eq!(wl.pop(), Some(1));
915        assert!(!wl.contains(1));
916        assert!(wl.contains(2));
917    }
918    #[test]
919    pub(super) fn test_dominator_tree() {
920        let mut dt = BashDominatorTree::new(5);
921        dt.set_idom(1, 0);
922        dt.set_idom(2, 0);
923        dt.set_idom(3, 1);
924        assert!(dt.dominates(0, 3));
925        assert!(dt.dominates(1, 3));
926        assert!(!dt.dominates(2, 3));
927        assert!(dt.dominates(3, 3));
928    }
929    #[test]
930    pub(super) fn test_liveness() {
931        let mut liveness = BashLivenessInfo::new(3);
932        liveness.add_def(0, 1);
933        liveness.add_use(1, 1);
934        assert!(liveness.defs[0].contains(&1));
935        assert!(liveness.uses[1].contains(&1));
936    }
937    #[test]
938    pub(super) fn test_constant_folding() {
939        assert_eq!(BashConstantFoldingHelper::fold_add_i64(3, 4), Some(7));
940        assert_eq!(BashConstantFoldingHelper::fold_div_i64(10, 0), None);
941        assert_eq!(BashConstantFoldingHelper::fold_div_i64(10, 2), Some(5));
942        assert_eq!(
943            BashConstantFoldingHelper::fold_bitand_i64(0b1100, 0b1010),
944            0b1000
945        );
946        assert_eq!(BashConstantFoldingHelper::fold_bitnot_i64(0), -1);
947    }
948    #[test]
949    pub(super) fn test_dep_graph() {
950        let mut g = BashDepGraph::new();
951        g.add_dep(1, 2);
952        g.add_dep(2, 3);
953        g.add_dep(1, 3);
954        assert_eq!(g.dependencies_of(2), vec![1]);
955        let topo = g.topological_sort();
956        assert_eq!(topo.len(), 3);
957        assert!(!g.has_cycle());
958        let pos: std::collections::HashMap<u32, usize> =
959            topo.iter().enumerate().map(|(i, &n)| (n, i)).collect();
960        assert!(pos[&1] < pos[&2]);
961        assert!(pos[&1] < pos[&3]);
962        assert!(pos[&2] < pos[&3]);
963    }
964}