bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
// REPL Explain Mode Module
//
// Task: REPL-005-002 - Explain Mode (Explain bash constructs interactively)
// Test Approach: RED → GREEN → REFACTOR → PROPERTY → MUTATION
//
// Quality targets:
// - Unit tests: 15+ scenarios
// - Integration tests: CLI explain mode with assert_cmd
// - Mutation score: ≥90%
// - Complexity: <10 per function

/// Explanation for a bash construct
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Explanation {
    /// Title of the construct
    pub title: String,
    /// Brief description
    pub description: String,
    /// Detailed explanation
    pub details: String,
    /// Example usage
    pub example: Option<String>,
}

impl Explanation {
    /// Create a new explanation
    pub fn new(
        title: impl Into<String>,
        description: impl Into<String>,
        details: impl Into<String>,
    ) -> Self {
        Self {
            title: title.into(),
            description: description.into(),
            details: details.into(),
            example: None,
        }
    }

    /// Add an example to the explanation
    pub fn with_example(mut self, example: impl Into<String>) -> Self {
        self.example = Some(example.into());
        self
    }

    /// Format explanation for display
    pub fn format(&self) -> String {
        let mut output = String::new();

        output.push_str(&format!("📖 {}\n", self.title));
        output.push_str(&format!("   {}\n\n", self.description));
        output.push_str(&format!("{}\n", self.details));

        if let Some(ref example) = self.example {
            output.push('\n');
            output.push_str(&format!("Example:\n{}\n", example));
        }

        output
    }
}

/// Explain a bash construct
///
/// This function analyzes bash code and returns an explanation
/// of the construct or syntax used.
///
/// # Examples
///
/// ```
/// use bashrs::repl::explain::explain_bash;
///
/// let explanation = explain_bash("${var:-default}");
/// assert!(explanation.is_some());
/// ```
pub fn explain_bash(input: &str) -> Option<Explanation> {
    let trimmed = input.trim();

    // Parameter expansion patterns
    if let Some(exp) = explain_parameter_expansion(trimmed) {
        return Some(exp);
    }

    // Control flow patterns
    if let Some(exp) = explain_control_flow(trimmed) {
        return Some(exp);
    }

    // Redirection patterns
    if let Some(exp) = explain_redirection(trimmed) {
        return Some(exp);
    }

    None
}

/// Check if input matches parameter expansion pattern with operator
fn matches_param_expansion(input: &str, operator: &str) -> bool {
    input.contains(operator) && input.starts_with("${") && input.ends_with('}')
}

/// Explain ${parameter:-word} - Use default value
fn explain_use_default() -> Explanation {
    Explanation::new(
        "Parameter Expansion: ${parameter:-word}",
        "Use Default Value",
        "If parameter is unset or null, expand to 'word'.\nThe original parameter remains unchanged.",
    )
    .with_example("  $ var=\"\"\n  $ echo \"${var:-fallback}\"  # Outputs: fallback\n  $ echo \"$var\"               # Still empty")
}

/// Explain ${parameter:=word} - Assign default value
fn explain_assign_default() -> Explanation {
    Explanation::new(
        "Parameter Expansion: ${parameter:=word}",
        "Assign Default Value",
        "If parameter is unset or null, assign 'word' to it.\nThen expand to the new value.",
    )
    .with_example("  $ unset var\n  $ echo \"${var:=fallback}\"  # Outputs: fallback\n  $ echo \"$var\"               # Now set to fallback")
}

/// Explain ${parameter:?word} - Display error if null/unset
fn explain_error_if_unset() -> Explanation {
    Explanation::new(
        "Parameter Expansion: ${parameter:?word}",
        "Display Error if Null/Unset",
        "If parameter is unset or null, print 'word' to stderr and exit.\nUseful for required parameters.",
    )
    .with_example("  $ unset var\n  $ echo \"${var:?Variable not set}\"  # Exits with error")
}

/// Explain ${parameter:+word} - Use alternate value
fn explain_use_alternate() -> Explanation {
    Explanation::new(
        "Parameter Expansion: ${parameter:+word}",
        "Use Alternate Value",
        "If parameter is set and non-null, expand to 'word'.\nOtherwise expand to nothing.",
    )
    .with_example("  $ var=\"set\"\n  $ echo \"${var:+present}\"  # Outputs: present\n  $ unset var\n  $ echo \"${var:+present}\"  # Outputs: (nothing)")
}

/// Explain ${#parameter} - String length
fn explain_string_length() -> Explanation {
    Explanation::new(
        "Parameter Expansion: ${#parameter}",
        "String Length",
        "Expands to the length of the parameter's value in characters.",
    )
    .with_example("  $ var=\"hello\"\n  $ echo \"${#var}\"  # Outputs: 5")
}

/// Explain parameter expansion constructs
fn explain_parameter_expansion(input: &str) -> Option<Explanation> {
    // ${var:-default} - Use default value
    if matches_param_expansion(input, ":-") {
        return Some(explain_use_default());
    }

    // ${var:=default} - Assign default value
    if matches_param_expansion(input, ":=") {
        return Some(explain_assign_default());
    }

    // ${var:?error} - Display error if null/unset
    if matches_param_expansion(input, ":?") {
        return Some(explain_error_if_unset());
    }

    // ${var:+alternate} - Use alternate value
    if matches_param_expansion(input, ":+") {
        return Some(explain_use_alternate());
    }

    // ${#var} - String length
    if input.starts_with("${#") && input.ends_with('}') {
        return Some(explain_string_length());
    }

    None
}

/// Control flow explanations: (prefix, syntax, category, description, example)
const CONTROL_FLOW_ENTRIES: &[(&str, &str, &str, &str, &str)] = &[
    (
        "for ",
        "For Loop: for name in words",
        "Iterate Over List",
        "Loop variable 'name' takes each value from the word list.\nExecutes commands for each iteration.",
        "  for file in *.txt; do\n    echo \"Processing: $file\"\n  done",
    ),
    (
        "if ",
        "If Statement: if condition; then commands; fi",
        "Conditional Execution",
        "Execute commands only if condition succeeds (exit status 0).\nOptional elif and else clauses for alternatives.",
        "  if [ -f file.txt ]; then\n    echo \"File exists\"\n  fi",
    ),
    (
        "while ",
        "While Loop: while condition; do commands; done",
        "Conditional Loop",
        "Execute commands repeatedly while condition succeeds.\nChecks condition before each iteration.",
        "  counter=0\n  while [ $counter -lt 5 ]; do\n    echo $counter\n    counter=$((counter + 1))\n  done",
    ),
    (
        "case ",
        "Case Statement: case word in pattern) commands;; esac",
        "Pattern Matching",
        "Match 'word' against patterns and execute corresponding commands.\nSupports glob patterns and multiple alternatives.",
        "  case $var in\n    start) echo \"Starting...\";;\n    stop)  echo \"Stopping...\";;\n    *)     echo \"Unknown\";;\n  esac",
    ),
];

/// Explain control flow constructs
fn explain_control_flow(input: &str) -> Option<Explanation> {
    for (prefix, syntax, category, description, example) in CONTROL_FLOW_ENTRIES {
        if input.starts_with(prefix) {
            return Some(Explanation::new(*syntax, *category, *description).with_example(*example));
        }
    }
    None
}

/// Explain redirection constructs
fn explain_redirection(input: &str) -> Option<Explanation> {
    // Output redirection >
    if input.contains(" > ") || input.ends_with('>') {
        return Some(
            Explanation::new(
                "Output Redirection: command > file",
                "Redirect Standard Output",
                "Redirects stdout to a file, overwriting existing content.\nUse >> to append instead."
            )
            .with_example("  echo \"text\" > file.txt   # Overwrite\n  echo \"more\" >> file.txt  # Append")
        );
    }

    // Input redirection <
    if input.contains(" < ") {
        return Some(
            Explanation::new(
                "Input Redirection: command < file",
                "Redirect Standard Input",
                "Redirects stdin to read from a file instead of keyboard.",
            )
            .with_example("  while read line; do\n    echo \"Line: $line\"\n  done < file.txt"),
        );
    }

    // Pipe |
    if input.contains(" | ") {
        return Some(
            Explanation::new(
                "Pipe: command1 | command2",
                "Connect Commands",
                "Redirects stdout of command1 to stdin of command2.\nEnables chaining multiple commands together."
            )
            .with_example("  cat file.txt | grep pattern | wc -l")
        );
    }

    // Here document <<
    if input.contains("<<") {
        return Some(
            Explanation::new(
                "Here Document: command << DELIMITER",
                "Multi-line Input",
                "Redirects multiple lines of input to a command.\nEnds at line containing only DELIMITER."
            )
            .with_example("  cat << EOF\n  Line 1\n  Line 2\n  EOF")
        );
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;

    // ===== RED PHASE: Unit Tests (These should FAIL initially) =====

    #[test]
    fn test_REPL_005_002_explain_parameter_expansion_use_default() {
        let result = explain_bash("${var:-default}");

        assert!(result.is_some(), "Should recognize parameter expansion");
        let explanation = result.unwrap();

        assert!(explanation.title.contains(":-"));
        assert!(explanation.description.contains("Default"));
        assert!(explanation.details.contains("unset or null"));
    }

    #[test]
    fn test_REPL_005_002_explain_parameter_expansion_assign_default() {
        let result = explain_bash("${var:=default}");

        assert!(result.is_some(), "Should recognize assign default");
        let explanation = result.unwrap();

        assert!(explanation.title.contains(":="));
        assert!(explanation.description.contains("Assign"));
    }

    #[test]
    fn test_REPL_005_002_explain_parameter_expansion_error() {
        let result = explain_bash("${var:?error message}");

        assert!(result.is_some(), "Should recognize error expansion");
        let explanation = result.unwrap();

        assert!(explanation.title.contains(":?"));
        assert!(explanation.description.contains("Error"));
    }

    #[test]
    fn test_REPL_005_002_explain_parameter_expansion_alternate() {
        let result = explain_bash("${var:+alternate}");

        assert!(result.is_some(), "Should recognize alternate expansion");
        let explanation = result.unwrap();

        assert!(explanation.title.contains(":+"));
        assert!(explanation.description.contains("Alternate"));
    }

    #[test]
    fn test_REPL_005_002_explain_string_length() {
        let result = explain_bash("${#var}");

        assert!(result.is_some(), "Should recognize string length");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("#"));
        assert!(explanation.description.contains("Length"));
    }

    #[test]
    fn test_REPL_005_002_explain_for_loop() {
        let result = explain_bash("for i in *.txt");

        assert!(result.is_some(), "Should recognize for loop");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("For"));
        assert!(explanation.description.contains("Iterate"));
    }

    #[test]
    fn test_REPL_005_002_explain_if_statement() {
        let result = explain_bash("if [ -f file ]");

        assert!(result.is_some(), "Should recognize if statement");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("If"));
        assert!(explanation.description.contains("Conditional"));
    }

    #[test]
    fn test_REPL_005_002_explain_while_loop() {
        let result = explain_bash("while true");

        assert!(result.is_some(), "Should recognize while loop");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("While"));
    }

    #[test]
    fn test_REPL_005_002_explain_case_statement() {
        let result = explain_bash("case $var in");

        assert!(result.is_some(), "Should recognize case statement");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("Case"));
        assert!(explanation.description.contains("Pattern"));
    }

    #[test]
    fn test_REPL_005_002_explain_output_redirection() {
        let result = explain_bash("echo test > file.txt");

        assert!(result.is_some(), "Should recognize output redirection");
        let explanation = result.unwrap();

        assert!(explanation.title.contains(">"));
        assert!(explanation.description.contains("Output"));
    }

    #[test]
    fn test_REPL_005_002_explain_input_redirection() {
        let result = explain_bash("cat < file.txt");

        assert!(result.is_some(), "Should recognize input redirection");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("<"));
        assert!(explanation.description.contains("Input"));
    }

    #[test]
    fn test_REPL_005_002_explain_pipe() {
        let result = explain_bash("cat file | grep pattern");

        assert!(result.is_some(), "Should recognize pipe");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("|") || explanation.title.contains("Pipe"));
        assert!(
            explanation.description.contains("Connect")
                || explanation.description.contains("Chain")
        );
    }

    #[test]
    fn test_REPL_005_002_explain_here_document() {
        let result = explain_bash("cat << EOF");

        assert!(result.is_some(), "Should recognize here document");
        let explanation = result.unwrap();

        assert!(explanation.title.contains("<<") || explanation.title.contains("Here"));
    }

    #[test]
    fn test_REPL_005_002_explain_unknown_returns_none() {
        let result = explain_bash("unknown_construct_xyz_123");

        assert!(
            result.is_none(),
            "Should return None for unknown constructs"
        );
    }

    #[test]
    fn test_REPL_005_002_explanation_format() {
        let explanation = Explanation::new(
            "Test Construct",
            "Brief description",
            "Detailed explanation here",
        )
        .with_example("  $ example command");

        let formatted = explanation.format();

        assert!(formatted.contains("📖 Test Construct"));
        assert!(formatted.contains("Brief description"));
        assert!(formatted.contains("Detailed explanation"));
        assert!(formatted.contains("Example:"));
        assert!(formatted.contains("$ example command"));
    }
}