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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
#![allow(clippy::unwrap_used)]
#![allow(unused_imports)]

use super::super::ast::Redirect;
use super::super::lexer::Lexer;
use super::super::parser::BashParser;
use super::super::semantic::SemanticAnalyzer;
use super::super::*;

/// Helper: tokenize input and assert tokens are non-empty.
/// Accepts parse errors gracefully (parser may not support all constructs yet).

#[test]
fn test_BUILTIN_011_pwd_symlink_resolution() {
    // DOCUMENTATION: pwd symlink handling with -L and -P
    // Important for determining canonical paths
    // -L follows symlinks (shows link path)
    // -P resolves symlinks (shows real path)

    let pwd_symlink = r#"
# If /home/user/project -> /mnt/storage/projects/myapp
cd /home/user/project

# Logical path (shows symlink)
pwd -L
# Output: /home/user/project

# Physical path (resolves symlink)
pwd -P
# Output: /mnt/storage/projects/myapp

# Get canonical path
canonical_path=$(pwd -P)
"#;

    let mut lexer = Lexer::new(pwd_symlink);
    match lexer.tokenize() {
        Ok(tokens) => {
            assert!(!tokens.is_empty(), "pwd symlink should tokenize");
            let _ = tokens; // Use tokens to satisfy type inference
                            // Symlink handling is POSIX
        }
        Err(_) => {
            // Test documents expected behavior
        }
    }

    // Use cases:
    // pwd -L: Show user-friendly path (with symlinks)
    // pwd -P: Get canonical path (resolve all symlinks)
}

#[test]
fn test_BUILTIN_011_pwd_edge_cases() {
    // DOCUMENTATION: Edge cases with pwd
    // Directory deleted, permissions, chroot

    let pwd_edge_cases = r#"
# Edge case: directory deleted
# mkdir /tmp/test && cd /tmp/test && rm -rf /tmp/test
# pwd  # May fail with error

# Edge case: no permissions
# cd /root/private (as non-root)
# pwd  # May fail with permission error

# Edge case: $PWD can be manually modified
PWD="/fake/path"
pwd    # Still shows real directory
echo $PWD  # Shows /fake/path

# Edge case: chroot environment
# pwd shows path relative to chroot, not actual system path
"#;

    let mut lexer = Lexer::new(pwd_edge_cases);
    match lexer.tokenize() {
        Ok(tokens) => {
            assert!(!tokens.is_empty(), "pwd edge cases should tokenize");
            let _ = tokens; // Use tokens to satisfy type inference
                            // Edge cases documented
        }
        Err(_) => {
            // Test documents expected behavior
        }
    }

    // Edge cases:
    // 1. Directory deleted: pwd may fail
    // 2. No permissions: pwd may fail
    // 3. $PWD modified: pwd still accurate
    // 4. Chroot: pwd relative to chroot
}

#[test]
fn test_BUILTIN_011_pwd_comparison_table() {
    // COMPREHENSIVE COMPARISON: POSIX vs Bash vs bashrs

    let pwd_comparison = r#"
# POSIX SUPPORTED (bashrs SUPPORTED):
pwd                  # Print current working directory
pwd -L               # Logical path (follow symlinks, default)
pwd -P               # Physical path (resolve symlinks)

# Common usage patterns:
current=$(pwd)       # Save current directory
old=$(pwd); cd /tmp; cd "$old"  # Save and restore

# Script directory pattern:
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Symlink handling:
# cd /path/to/symlink
pwd -L               # Shows symlink path
pwd -P               # Shows real path

# pwd vs $PWD:
echo $(pwd)          # Command (always accurate)
echo $PWD            # Variable (can be modified)

# Best practices:
dir="$(pwd)"         # Quote for safety
[ "$(pwd)" = "/etc" ]  # Directory check
canonical="$(pwd -P)"  # Get canonical path

# Exit status:
if pwd; then
    echo "Success"
fi
"#;

    let mut lexer = Lexer::new(pwd_comparison);
    match lexer.tokenize() {
        Ok(tokens) => {
            assert!(!tokens.is_empty(), "pwd comparison should tokenize");
            let _ = tokens; // Use tokens to satisfy type inference
        }
        Err(_) => {
            // Test documents comprehensive pwd behavior
        }
    }

    // SUMMARY
    // pwd is POSIX-COMPLIANT and FULLY SUPPORTED in bashrs
    // pwd prints current working directory
    // pwd -L follows symlinks (logical path, default)
    // pwd -P resolves symlinks (physical path)
    // Use pwd for portability, $PWD for efficiency
    // pwd is deterministic (always returns current directory)
}

// ============================================================================
// BUILTIN-016: test / [ Command (POSIX SUPPORTED - HIGH PRIORITY)
// ============================================================================

// DOCUMENTATION: test / [ is SUPPORTED (POSIX builtin, HIGH priority)
//
// test evaluates conditional expressions
// [ is an alias for test (closing ] required)
// [[ ]] is a bash extension (NOT SUPPORTED, use [ ] for portability)
//
// POSIX test supports:
// - File tests: -f (file), -d (dir), -e (exists), -r (read), -w (write), -x (exec)
// - String tests: -z (zero length), -n (non-zero), = (equal), != (not equal)
// - Integer tests: -eq, -ne, -lt, -le, -gt, -ge
// - Logical: ! (not), -a (and), -o (or)
//
// Bash extensions NOT SUPPORTED:
// - [[ ]] compound command (use [ ] instead)
// - =~ regex matching (use grep or sed)
// - Pattern matching with == (use case statement)
// - < > string comparison (use [ "$a" \< "$b" ] with backslash escaping)
//
// INPUT (bash with extensions):
//   [[ -f "file.txt" && "$user" == "admin" ]] → [ -f "file.txt" ] && [ "$user" = "admin" ]
//
// RUST TRANSFORMATION:
//   std::path::Path::new("file.txt").is_file() && user == "admin"
//
// COMPARISON TABLE: test / [ POSIX vs Bash
// ┌─────────────────────────────┬──────────────┬────────────────────────────┐
// │ Feature                     │ POSIX Status │ Purification Strategy      │
// ├─────────────────────────────┼──────────────┼────────────────────────────┤
// │ [ -f "file" ]               │ SUPPORTED    │ Keep as-is                 │
// │ [ -d "dir" ]                │ SUPPORTED    │ Keep as-is                 │
// │ [ -e "path" ]               │ SUPPORTED    │ Keep as-is                 │
// │ [ -r/-w/-x "file" ]         │ SUPPORTED    │ Keep as-is                 │
// │ [ -z "$str" ]               │ SUPPORTED    │ Keep as-is                 │
// │ [ -n "$str" ]               │ SUPPORTED    │ Keep as-is                 │
// │ [ "$a" = "$b" ]             │ SUPPORTED    │ Keep as-is                 │
// │ [ "$a" != "$b" ]            │ SUPPORTED    │ Keep as-is                 │
// │ [ "$a" -eq "$b" ]           │ SUPPORTED    │ Keep as-is                 │
// │ [ "$a" -ne/-lt/-le/-gt/-ge ]│ SUPPORTED    │ Keep as-is                 │
// │ [ ! -f "file" ]             │ SUPPORTED    │ Keep as-is                 │
// │ [ -f "a" -a -f "b" ]        │ SUPPORTED    │ Keep as-is                 │
// │ [ -f "a" -o -f "b" ]        │ SUPPORTED    │ Keep as-is                 │
// │ [[ -f "file" ]]             │ NOT SUPPORT  │ Replace [[ ]] with [ ]     │
// │ [[ "$a" == "$b" ]]          │ NOT SUPPORT  │ Replace == with =          │
// │ [[ "$a" =~ regex ]]         │ NOT SUPPORT  │ Use grep or sed            │
// │ [[ "$a" < "$b" ]]           │ NOT SUPPORT  │ Use [ "$a" \< "$b" ]       │
// │ [ -f "a" && -f "b" ]        │ NOT POSIX    │ Split: [ -f "a" ] && [ ]   │
// └─────────────────────────────┴──────────────┴────────────────────────────┘
//
// PURIFICATION EXAMPLES:
//   1. [[ -f "file.txt" ]] → [ -f "file.txt" ]
//   2. [[ "$user" == "admin" ]] → [ "$user" = "admin" ]
//   3. [[ "$email" =~ regex ]] → printf '%s' "$email" | grep -qE 'regex'
//   4. [ -f "a" && -f "b" ] → [ -f "a" ] && [ -f "b" ]
//   5. [[ "$a" < "$b" ]] → [ "$a" \< "$b" ]
//
// PRIORITY: HIGH - test is fundamental to all conditional logic
// POSIX: IEEE Std 1003.1-2001 test utility
const BUILTIN_016_TEST_COMMAND_INPUT: &str = r#"
if [ -f "file.txt" ]; then
    echo "File exists"
fi

if [ -d "/tmp" ]; then
    echo "Directory exists"
fi

if [ "$user" = "admin" ]; then
    echo "Admin user"
fi

if [ "$count" -gt 10 ]; then
    echo "Count is greater than 10"
fi
"#;

#[test]
fn test_BUILTIN_016_test_command_supported() {
    assert_tokenizes(
        BUILTIN_016_TEST_COMMAND_INPUT,
        "test command should tokenize successfully",
    );
}

// DOCUMENTATION: File test operators (POSIX)
// -f FILE (regular file), -d (dir), -e (exists), -r (readable),
// -w (writable), -x (executable), -s (non-empty), -L (symlink)
// RUST: std::path::Path::new("/etc/passwd").is_file()
const BUILTIN_016_FILE_TESTS_INPUT: &str = r#"
# File type tests
if [ -f "/etc/passwd" ]; then echo "regular file"; fi
if [ -d "/tmp" ]; then echo "directory"; fi
if [ -e "/dev/null" ]; then echo "exists"; fi
if [ -L "/usr/bin/vi" ]; then echo "symlink"; fi

# Permission tests
if [ -r "file.txt" ]; then echo "readable"; fi
if [ -w "file.txt" ]; then echo "writable"; fi
if [ -x "script.sh" ]; then echo "executable"; fi

# Size test
if [ -s "data.txt" ]; then echo "non-empty"; fi
"#;

#[test]
fn test_BUILTIN_016_test_file_tests() {
    assert_tokenizes(
        BUILTIN_016_FILE_TESTS_INPUT,
        "file test operators should tokenize",
    );
}

// DOCUMENTATION: String test operators (POSIX)
// -z STRING (zero length), -n (non-zero), = (equal), != (not equal)
// NOTE: Use = not == for POSIX portability (== is bash-only)
// Purification: [[ "$name" == "alice" ]] → [ "$name" = "alice" ]
const BUILTIN_016_STRING_TESTS_INPUT: &str = r#"
# Empty/non-empty tests
if [ -z "$empty_var" ]; then echo "empty"; fi
if [ -n "$non_empty_var" ]; then echo "non-empty"; fi

# String equality (POSIX uses =, not ==)
if [ "$user" = "admin" ]; then echo "admin user"; fi
if [ "$status" != "error" ]; then echo "ok"; fi

# Always quote variables in tests
if [ -z "$var" ]; then echo "var is empty"; fi
if [ "$a" = "$b" ]; then echo "equal"; fi
"#;

#[test]
fn test_BUILTIN_016_test_string_tests() {
    assert_tokenizes(
        BUILTIN_016_STRING_TESTS_INPUT,
        "string test operators should tokenize",
    );
}

// DOCUMENTATION: Integer comparison operators (POSIX)
// -eq (equal), -ne (not equal), -lt (less), -le (less/equal),
// -gt (greater), -ge (greater/equal)
// NOTE: Use -eq not == for integer comparison
// RUST: count > 10
const BUILTIN_016_INTEGER_TESTS_INPUT: &str = r#"
# Integer comparisons
if [ "$count" -eq 0 ]; then echo "zero"; fi
if [ "$count" -ne 0 ]; then echo "non-zero"; fi
if [ "$count" -lt 10 ]; then echo "less than 10"; fi
if [ "$count" -le 10 ]; then echo "at most 10"; fi
if [ "$count" -gt 10 ]; then echo "greater than 10"; fi
if [ "$count" -ge 10 ]; then echo "at least 10"; fi

# Common patterns
if [ "$retries" -lt "$max_retries" ]; then
    echo "Retry available"
fi

if [ "$exit_code" -ne 0 ]; then
    echo "Command failed"
fi
"#;

#[test]
fn test_BUILTIN_016_test_integer_tests() {
    assert_tokenizes(
        BUILTIN_016_INTEGER_TESTS_INPUT,
        "integer test operators should tokenize",
    );
}

// DOCUMENTATION: Logical operators for test (POSIX)
// ! EXPR (NOT), EXPR1 -a EXPR2 (AND), EXPR1 -o EXPR2 (OR)
// MODERN POSIX: split into multiple [ ] tests with && and ||
// OLD POSIX: combine with -a/-o inside single [ ] (deprecated)
// Purification: [[ -f "file" && -r "file" ]] → [ -f "file" ] && [ -r "file" ]
const BUILTIN_016_LOGICAL_TESTS_INPUT: &str = r#"
# Logical NOT
if [ ! -f "missing.txt" ]; then echo "file does not exist"; fi

# Logical AND (modern style - preferred)
if [ -f "file.txt" ] && [ -r "file.txt" ]; then
    cat file.txt
fi

# Logical OR (modern style - preferred)
if [ "$status" = "ok" ] || [ "$status" = "success" ]; then
    echo "Operation succeeded"
fi

# Logical AND (old style - deprecated but valid)
if [ -f "file.txt" -a -r "file.txt" ]; then
    cat file.txt
fi

# Logical OR (old style - deprecated but valid)
if [ "$a" = "1" -o "$a" = "2" ]; then
    echo "a is 1 or 2"
fi

# Complex logic with negation
if [ ! -z "$var" ] && [ -f "$var" ]; then
    echo "$var is a non-empty filename"
fi
"#;

#[test]
fn test_BUILTIN_016_test_logical_operators() {
    assert_tokenizes(
        BUILTIN_016_LOGICAL_TESTS_INPUT,
        "logical operators should tokenize",
    );
}

// DOCUMENTATION: Bash [[ ]] extensions (NOT SUPPORTED)
// [[ ]] is a bash keyword, not a POSIX builtin.
// BASH EXTENSIONS (NOT SUPPORTED):
//   1. [[ ]] compound command → use [ ] instead
//   2. == pattern matching → use = for string equality
//   3. =~ regex matching → use grep, sed, or case
//   4. < > string comparison without escaping → use \< \>
//   5. && || inside [[ ]] → split into separate [ ] tests
const BUILTIN_016_BASH_EXTENSIONS_INPUT: &str = r#"
# BASH EXTENSION: [[ ]] compound command (NOT SUPPORTED)
# Purify: Replace [[ ]] with [ ]
# if [[ -f "file.txt" ]]; then echo "exists"; fi
# →
if [ -f "file.txt" ]; then echo "exists"; fi

# BASH EXTENSION: == operator (NOT SUPPORTED)
# Purify: Replace == with =
# if [[ "$user" == "admin" ]]; then echo "admin"; fi
# →
if [ "$user" = "admin" ]; then echo "admin"; fi

# BASH EXTENSION: =~ regex (NOT SUPPORTED)
# Purify: Use grep instead
# if [[ "$email" =~ ^[a-z]+@[a-z]+\.com$ ]]; then echo "valid"; fi
# →
if printf '%s' "$email" | grep -qE '^[a-z]+@[a-z]+\.com$'; then
    echo "valid"
fi

# BASH EXTENSION: Pattern matching with == (NOT SUPPORTED)
# Purify: Use case statement
# if [[ "$file" == *.txt ]]; then echo "text file"; fi
# →
case "$file" in
    *.txt)
        echo "text file"
        ;;
esac

# BASH EXTENSION: < > without escaping (NOT SUPPORTED)
# Purify: Add backslash escaping
# if [[ "$a" < "$b" ]]; then echo "less"; fi
# →
if [ "$a" \< "$b" ]; then echo "less"; fi
"#;

#[test]
fn test_BUILTIN_016_test_bash_extensions_not_supported() {
    assert_tokenizes(
        BUILTIN_016_BASH_EXTENSIONS_INPUT,
        "bash extension examples should tokenize",
    );
}

// DOCUMENTATION: Common test patterns in POSIX scripts
// 1. Check file exists before reading
// 2. Check variable is set
// 3. Check variable is unset or empty
// 4. Check exit status
// 5. Check multiple conditions
// 6. Check for errors (defensive programming)
// 7. Alternative values
const BUILTIN_016_COMMON_PATTERNS_INPUT: &str = r#"
# Pattern 1: Safe file operations
if [ -f "config.sh" ]; then
    . config.sh
fi

# Pattern 2: Variable validation
if [ -z "$REQUIRED_VAR" ]; then
    echo "Error: REQUIRED_VAR is not set"
    exit 1
fi

# Pattern 3: Default values
if [ -z "$PORT" ]; then
    PORT=8080
fi

# Pattern 4: Error checking
command_that_might_fail
if [ "$?" -ne 0 ]; then
    echo "Command failed with exit code $?"
    exit 1
fi

# Pattern 5: Defensive programming
if [ ! -d "$install_dir" ]; then
    echo "Error: Install directory does not exist: $install_dir"
    exit 1
fi

# Pattern 6: Multi-condition validation
if [ -f "$script" ] && [ -r "$script" ] && [ -x "$script" ]; then
    "$script"
else
    echo "Error: $script is not a readable executable file"
    exit 1
fi

# Pattern 7: Alternative values
if [ -n "$CUSTOM_PATH" ]; then
    PATH="$CUSTOM_PATH"
else
    PATH="/usr/local/bin:/usr/bin:/bin"
fi
"#;

#[test]
fn test_BUILTIN_016_test_common_patterns() {
    assert_tokenizes(
        BUILTIN_016_COMMON_PATTERNS_INPUT,
        "common test patterns should tokenize",
    );
}