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
#![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: assert that BashParser handles the input without panicking.
/// Accepts both successful parses and parse errors (documentation tests
/// only verify the parser doesn't crash, not that the input is valid).
#[test]
fn test_PARAM_SPEC_002_exit_status_clobbering() {
let clobbering_issue = r#"
# BAD: $? clobbered by [ command
cmd
if [ $? -eq 0 ]; then # This tests if [ succeeded, not cmd!
echo "Wrong"
fi
# GOOD: Capture $? immediately
cmd
STATUS=$?
if [ $STATUS -eq 0 ]; then
echo "Correct"
fi
# BETTER: Direct conditional
if cmd; then
echo "Best practice"
fi
"#;
assert_parses_without_panic(
clobbering_issue,
"$? clobbering behavior is POSIX-compliant",
);
}
#[test]
fn test_PARAM_SPEC_002_exit_status_functions() {
// DOCUMENTATION: $? with functions (POSIX)
//
// Functions return exit status like commands:
// - Explicit: return N (0-255)
// - Implicit: exit status of last command
//
// $ my_function() {
// $ cmd
// $ return $? # Explicit return
// $ }
// $
// $ my_function
// $ echo $? # Function's return value
let function_exit = r#"
check_file() {
if [ -f "$1" ]; then
return 0
else
return 1
fi
}
# Implicit return (last command)
process_data() {
validate_input
transform_data
save_output # Function returns this command's status
}
# Using function status
check_file "/tmp/data.txt"
if [ $? -eq 0 ]; then
echo "File exists"
fi
# Better: Direct conditional
if check_file "/tmp/data.txt"; then
echo "File exists"
fi
"#;
let result = BashParser::new(function_exit);
match result {
Ok(mut parser) => {
let parse_result = parser.parse();
assert!(
parse_result.is_ok() || parse_result.is_err(),
"$? with functions is POSIX-compliant"
);
}
Err(_) => {
// Parse error acceptable
}
}
}
#[test]
fn test_PARAM_SPEC_002_exit_status_subshells() {
// DOCUMENTATION: $? with subshells and command substitution (POSIX)
//
// Subshells and command substitution preserve exit status:
//
// Subshell:
// $ ( cmd1; cmd2 )
// $ echo $? # Exit status of cmd2
//
// Command substitution (capture output, lose status):
// $ OUTPUT=$(cmd)
// $ echo $? # Always 0 if assignment succeeded
//
// To capture both output and status:
// $ OUTPUT=$(cmd)
// $ STATUS=$? # This is too late! Already clobbered
//
// Better: Set -e or check inline:
// $ OUTPUT=$(cmd) || { echo "Failed"; exit 1; }
let subshell_exit = r#"
# Subshell exit status
( cmd1; cmd2 )
echo "Subshell status: $?"
# Command substitution loses status
OUTPUT=$(cmd)
echo $? # This is assignment status, not cmd status!
# Capture output and check status inline
OUTPUT=$(cmd) || {
echo "Command failed"
exit 1
}
# Alternative: set -e (exit on any error)
set -e
OUTPUT=$(cmd) # Will exit script if cmd fails
"#;
let result = BashParser::new(subshell_exit);
match result {
Ok(mut parser) => {
let parse_result = parser.parse();
assert!(
parse_result.is_ok() || parse_result.is_err(),
"$? with subshells is POSIX-compliant"
);
}
Err(_) => {
// Parse error acceptable
}
}
}
#[test]
fn test_PARAM_SPEC_002_exit_status_common_use_cases() {
// DOCUMENTATION: Common $? use cases (POSIX)
//
// Use Case 1: Error handling
// $ cmd
// $ if [ $? -ne 0 ]; then
// $ echo "Error occurred"
// $ exit 1
// $ fi
//
// Use Case 2: Multiple status checks
// $ cmd1
// $ STATUS1=$?
// $ cmd2
// $ STATUS2=$?
// $ if [ $STATUS1 -ne 0 ] || [ $STATUS2 -ne 0 ]; then
// $ echo "One or both failed"
// $ fi
//
// Use Case 3: Logging
// $ cmd
// $ STATUS=$?
// $ log_message "Command exited with status $STATUS"
// $ [ $STATUS -eq 0 ] || exit $STATUS
let common_uses = r#"
# Use Case 1: Error handling
deploy_app
if [ $? -ne 0 ]; then
echo "Deployment failed"
rollback_changes
exit 1
fi
# Use Case 2: Multiple checks
backup_database
DB_STATUS=$?
backup_files
FILE_STATUS=$?
if [ $DB_STATUS -ne 0 ] || [ $FILE_STATUS -ne 0 ]; then
echo "Backup failed"
send_alert
exit 1
fi
# Use Case 3: Logging with status
critical_operation
STATUS=$?
log_event "Operation completed with status $STATUS"
if [ $STATUS -ne 0 ]; then
send_alert "Critical operation failed: $STATUS"
exit $STATUS
fi
"#;
let result = BashParser::new(common_uses);
match result {
Ok(mut parser) => {
let parse_result = parser.parse();
assert!(
parse_result.is_ok() || parse_result.is_err(),
"Common $? patterns are POSIX-compliant"
);
}
Err(_) => {
// Parse error acceptable
}
}
}
// DOCUMENTATION: Exit status comparison (POSIX vs Bash)
// $? is POSIX-compliant, 0-255 range, clobbered by every command.
// Rust mapping: std::process::Command .status() .code()
// bashrs: SUPPORTED, no transformation needed, preserve as-is.
#[test]
fn test_PARAM_SPEC_002_exit_status_comparison_table() {
let comparison_example = r#"
# POSIX: $? fully supported
cmd
echo "Exit: $?"
# POSIX: Capture and use
cmd
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "Failed with code $STATUS"
exit $STATUS
fi
# POSIX: set -o pipefail (supported in bash, dash, ash)
set -o pipefail
cmd1 | cmd2 | cmd3
if [ $? -ne 0 ]; then
echo "Pipeline failed"
fi
# Bash-only: PIPESTATUS (NOT SUPPORTED)
# cmd1 | cmd2 | cmd3
# echo "${PIPESTATUS[@]}" # bashrs doesn't support this
"#;
assert_parses_without_panic(comparison_example, "$? comparison documented");
}
// Summary:
// $? (exit status): FULLY SUPPORTED (POSIX)
// Range: 0-255 (0=success, non-zero=failure)
// Special codes: 126 (not executable), 127 (not found), 128+N (signal)
// Clobbering: Updated after every command
// Best practice: Capture immediately or use direct conditionals
// PIPESTATUS: NOT SUPPORTED (bash extension)
// pipefail: SUPPORTED (POSIX, available in bash/dash/ash)
// ============================================================================
// PARAM-SPEC-003: $$ Process ID (POSIX, but NON-DETERMINISTIC - PURIFY)
// ============================================================================
// DOCUMENTATION: $$ is POSIX but NON-DETERMINISTIC (must purify)
// $$ contains the process ID of the current shell. Changes every run.
// Purification: replace $$ with fixed identifier, use mktemp for temp files.
#[test]
fn test_PARAM_SPEC_003_process_id_non_deterministic() {
let process_id = r#"
echo "Process ID: $$"
echo "Script PID: $$"
"#;
assert_parses_without_panic(
process_id,
"$$ is POSIX-compliant but NON-DETERMINISTIC (must purify)",
);
}
#[test]
fn test_PARAM_SPEC_003_process_id_temp_files() {
// DOCUMENTATION: Common anti-pattern - $$ for temp files
//
// ANTI-PATTERN (non-deterministic):
// $ TMPFILE=/tmp/myapp.$$
// $ echo "data" > /tmp/script.$$.log
// $ rm -f /tmp/output.$$
//
// Problem: File names change every run
// - First run: /tmp/myapp.12345
// - Second run: /tmp/myapp.67890
// - Third run: /tmp/myapp.23456
//
// This breaks:
// - Determinism (file names unpredictable)
// - Idempotency (can't clean up old files reliably)
// - Testing (can't assert on specific file names)
//
// POSIX alternatives (deterministic):
// 1. Use mktemp (creates unique temp file safely):
// $ TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
//
// 2. Use fixed name with script name:
// $ TMPFILE="/tmp/myapp.tmp"
//
// 3. Use XDG directories:
// $ TMPFILE="${XDG_RUNTIME_DIR:-/tmp}/myapp.tmp"
//
// 4. Use script name from $0:
// $ TMPFILE="/tmp/$(basename "$0").tmp"
let temp_file_pattern = r#"
# ANTI-PATTERN: Non-deterministic temp files
TMPFILE=/tmp/myapp.$$
echo "data" > /tmp/script.$$.log
rm -f /tmp/output.$$
# BETTER: Use mktemp (deterministic, safe)
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
# BETTER: Use fixed name
TMPFILE="/tmp/myapp.tmp"
# BETTER: Use script name
TMPFILE="/tmp/$(basename "$0").tmp"
"#;
let result = BashParser::new(temp_file_pattern);
match result {
Ok(mut parser) => {
let parse_result = parser.parse();
assert!(
parse_result.is_ok() || parse_result.is_err(),
"$$ for temp files is non-deterministic anti-pattern"
);
}
Err(_) => {
// Parse error acceptable
}
}
}
#[test]
include!("part3_6_param_spec.rs");