rable 0.1.1

A Rust implementation of the Parable bash parser — complete GNU Bash 5.3-compatible parsing with Python bindings
Documentation
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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
# Iteration 7: If statements - if/then/elif/else/fi
# Comprehensive edge cases from tree-sitter-bash, Oil Shell, POSIX spec

# === Basic if/then/fi ===

=== simple if
if true; then echo yes; fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== if with test command
if test -f file; then cat file; fi
---
(if (command (word "test") (word "-f") (word "file")) (command (word "cat") (word "file")))
---


=== if with bracket test
if [ -f file ]; then cat file; fi
---
(if (command (word "[") (word "-f") (word "file") (word "]")) (command (word "cat") (word "file")))
---


=== if with pipeline condition
if echo foo | grep foo; then echo found; fi
---
(if (pipe (command (word "echo") (word "foo")) (command (word "grep") (word "foo"))) (command (word "echo") (word "found")))
---


=== if with list condition
if cmd1 && cmd2; then echo both; fi
---
(if (and (command (word "cmd1")) (command (word "cmd2"))) (command (word "echo") (word "both")))
---


=== if with or condition
if cmd1 || cmd2; then echo either; fi
---
(if (or (command (word "cmd1")) (command (word "cmd2"))) (command (word "echo") (word "either")))
---


# === Multiple commands in condition (from tree-sitter-bash) ===

=== multiple commands in condition
if foo; bar; then baz; fi
---
(if (semi (command (word "foo")) (command (word "bar"))) (command (word "baz")))
---


=== three commands in condition
if a; b; c; then d; fi
---
(if (semi (semi (command (word "a")) (command (word "b"))) (command (word "c"))) (command (word "d")))
---


# === If/else ===

=== simple if else
if true; then echo yes; else echo no; fi
---
(if (command (word "true")) (command (word "echo") (word "yes")) (command (word "echo") (word "no")))
---


=== if else with test
if [ "$x" = "y" ]; then echo match; else echo nomatch; fi
---
(if (command (word "[") (word "\"$x\"") (word "=") (word "\"y\"") (word "]")) (command (word "echo") (word "match")) (command (word "echo") (word "nomatch")))
---


=== if else with complex bodies
if test -d dir; then cd dir; ls; else mkdir dir; fi
---
(if (command (word "test") (word "-d") (word "dir")) (semi (command (word "cd") (word "dir")) (command (word "ls"))) (command (word "mkdir") (word "dir")))
---


=== if else multiple commands both branches
if test 1; then echo a; echo b; else echo c; echo d; fi
---
(if (command (word "test") (word "1")) (semi (command (word "echo") (word "a")) (command (word "echo") (word "b"))) (semi (command (word "echo") (word "c")) (command (word "echo") (word "d"))))
---


# === Elif chains ===

=== simple elif
if test 1; then echo one; elif test 2; then echo two; fi
---
(if (command (word "test") (word "1")) (command (word "echo") (word "one")) (if (command (word "test") (word "2")) (command (word "echo") (word "two"))))
---


=== elif with else
if test 1; then echo one; elif test 2; then echo two; else echo other; fi
---
(if (command (word "test") (word "1")) (command (word "echo") (word "one")) (if (command (word "test") (word "2")) (command (word "echo") (word "two")) (command (word "echo") (word "other"))))
---


=== multiple elif
if test 1; then echo one; elif test 2; then echo two; elif test 3; then echo three; fi
---
(if (command (word "test") (word "1")) (command (word "echo") (word "one")) (if (command (word "test") (word "2")) (command (word "echo") (word "two")) (if (command (word "test") (word "3")) (command (word "echo") (word "three")))))
---


=== multiple elif with else
if test 1; then echo one; elif test 2; then echo two; elif test 3; then echo three; else echo other; fi
---
(if (command (word "test") (word "1")) (command (word "echo") (word "one")) (if (command (word "test") (word "2")) (command (word "echo") (word "two")) (if (command (word "test") (word "3")) (command (word "echo") (word "three")) (command (word "echo") (word "other")))))
---


=== four elif chain
if a; then echo a; elif b; then echo b; elif c; then echo c; elif d; then echo d; else echo e; fi
---
(if (command (word "a")) (command (word "echo") (word "a")) (if (command (word "b")) (command (word "echo") (word "b")) (if (command (word "c")) (command (word "echo") (word "c")) (if (command (word "d")) (command (word "echo") (word "d")) (command (word "echo") (word "e"))))))
---


=== elif with complex conditions
if cat file | grep a; then echo a; elif cat file | grep b; then echo b; else exit; fi
---
(if (pipe (command (word "cat") (word "file")) (command (word "grep") (word "a"))) (command (word "echo") (word "a")) (if (pipe (command (word "cat") (word "file")) (command (word "grep") (word "b"))) (command (word "echo") (word "b")) (command (word "exit"))))
---


# === Nested if ===

=== nested if in then
if test 1; then if test 2; then echo both; fi; fi
---
(if (command (word "test") (word "1")) (if (command (word "test") (word "2")) (command (word "echo") (word "both"))))
---


=== nested if in else
if test 1; then echo one; else if test 2; then echo two; fi; fi
---
(if (command (word "test") (word "1")) (command (word "echo") (word "one")) (if (command (word "test") (word "2")) (command (word "echo") (word "two"))))
---


=== deeply nested if
if a; then if b; then if c; then echo deep; fi; fi; fi
---
(if (command (word "a")) (if (command (word "b")) (if (command (word "c")) (command (word "echo") (word "deep")))))
---


=== nested if with elif
if a; then if b; then echo ab; elif c; then echo ac; fi; fi
---
(if (command (word "a")) (if (command (word "b")) (command (word "echo") (word "ab")) (if (command (word "c")) (command (word "echo") (word "ac")))))
---


=== nested if in elif branch
if a; then echo a; elif b; then if c; then echo bc; fi; fi
---
(if (command (word "a")) (command (word "echo") (word "a")) (if (command (word "b")) (if (command (word "c")) (command (word "echo") (word "bc")))))
---


# === If with newlines (alternative to semicolons) ===

=== if with newline before then
if true
then echo yes; fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== if with newlines throughout
if true
then
echo yes
fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== if else with newlines
if false
then
echo yes
else
echo no
fi
---
(if (command (word "false")) (command (word "echo") (word "yes")) (command (word "echo") (word "no")))
---


=== elif with newlines
if a
then
echo a
elif b
then
echo b
else
echo c
fi
---
(if (command (word "a")) (command (word "echo") (word "a")) (if (command (word "b")) (command (word "echo") (word "b")) (command (word "echo") (word "c"))))
---


=== multiline then body
if true
then
echo one
echo two
echo three
fi
---
(if (command (word "true")) (semi (semi (command (word "echo") (word "one")) (command (word "echo") (word "two"))) (command (word "echo") (word "three"))))
---


# === If in pipelines and lists ===

=== if in pipeline
if true; then echo yes; fi | cat
---
(pipe (if (command (word "true")) (command (word "echo") (word "yes"))) (command (word "cat")))
---


=== if in and list
if true; then echo yes; fi && echo done
---
(and (if (command (word "true")) (command (word "echo") (word "yes"))) (command (word "echo") (word "done")))
---


=== if in or list
if false; then echo yes; fi || echo fallback
---
(or (if (command (word "false")) (command (word "echo") (word "yes"))) (command (word "echo") (word "fallback")))
---


=== if in background
if true; then sleep 10; fi &
---
(background (if (command (word "true")) (command (word "sleep") (word "10"))))
---


=== if after semicolon
echo before; if true; then echo yes; fi
---
(semi (command (word "echo") (word "before")) (if (command (word "true")) (command (word "echo") (word "yes"))))
---


=== multiple if in sequence
if a; then echo a; fi; if b; then echo b; fi
---
(semi (if (command (word "a")) (command (word "echo") (word "a"))) (if (command (word "b")) (command (word "echo") (word "b"))))
---


=== if in complex pipeline
echo start | if true; then cat; fi | wc -l
---
(pipe (command (word "echo") (word "start")) (pipe (if (command (word "true")) (command (word "cat"))) (command (word "wc") (word "-l"))))
---


# === Subshell and brace group in condition ===

=== subshell in condition
if (true); then echo yes; fi
---
(if (subshell (command (word "true"))) (command (word "echo") (word "yes")))
---


=== subshell with commands in condition
if (echo foo; true); then echo yes; fi
---
(if (subshell (semi (command (word "echo") (word "foo")) (command (word "true")))) (command (word "echo") (word "yes")))
---


=== brace group in condition
if { true; }; then echo yes; fi
---
(if (brace-group (command (word "true"))) (command (word "echo") (word "yes")))
---


=== brace group with multiple commands in condition
if { cmd1; cmd2; }; then echo yes; fi
---
(if (brace-group (semi (command (word "cmd1")) (command (word "cmd2")))) (command (word "echo") (word "yes")))
---


# === Subshell and brace group in body ===

=== subshell in then body
if true; then (echo in subshell); fi
---
(if (command (word "true")) (subshell (command (word "echo") (word "in") (word "subshell"))))
---


=== brace group in then body
if true; then { echo in brace; }; fi
---
(if (command (word "true")) (brace-group (command (word "echo") (word "in") (word "brace"))))
---


=== subshell in else body
if false; then echo yes; else (echo no); fi
---
(if (command (word "false")) (command (word "echo") (word "yes")) (subshell (command (word "echo") (word "no"))))
---


# === Complex conditions ===

=== if with negation
if ! test -f file; then echo missing; fi
---
(if (negation (command (word "test") (word "-f") (word "file"))) (command (word "echo") (word "missing")))
---


=== if with double bracket
if [[ -f file ]]; then cat file; fi
---
(if (cond (cond-unary "-f" (cond-term "file"))) (command (word "cat") (word "file")))
---


=== double bracket with pattern
if [[ "$x" == *.txt ]]; then echo text; fi
---
(if (cond (cond-binary "==" (cond-term ""$x"") (cond-term "*.txt"))) (command (word "echo") (word "text")))
---


=== double bracket regex
if [[ "$x" =~ ^[0-9]+$ ]]; then echo number; fi
---
(if (cond (cond-binary "=~" (cond-term ""$x"") (cond-term "^[0-9]+$"))) (command (word "echo") (word "number")))
---


=== test with string comparison
if [ "$(uname)" = "Darwin" ]; then echo mac; fi
---
(if (command (word "[") (word "\"$(uname)\"") (word "=") (word "\"Darwin\"") (word "]")) (command (word "echo") (word "mac")))
---


=== test with numeric comparison
if [ "$x" -gt 10 ]; then echo big; fi
---
(if (command (word "[") (word "\"$x\"") (word "-gt") (word "10") (word "]")) (command (word "echo") (word "big")))
---


=== test with multiple conditions
if [ -f file ] && [ -r file ]; then cat file; fi
---
(if (and (command (word "[") (word "-f") (word "file") (word "]")) (command (word "[") (word "-r") (word "file") (word "]"))) (command (word "cat") (word "file")))
---


=== double bracket with and
if [[ -f file ]] && [[ -r file ]]; then cat file; fi
---
(if (and (cond (cond-unary "-f" (cond-term "file"))) (cond (cond-unary "-r" (cond-term "file")))) (command (word "cat") (word "file")))
---


=== double bracket with or
if [[ -f a ]] || [[ -f b ]]; then echo found; fi
---
(if (or (cond (cond-unary "-f" (cond-term "a"))) (cond (cond-unary "-f" (cond-term "b")))) (command (word "echo") (word "found")))
---


# NOTE: (( )) arithmetic expressions require special parsing
# Currently parsed as nested subshells - proper (( )) support is future work

# === Whitespace variations ===

=== if no space after semicolons
if true;then echo yes;fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== if extra spaces
if   true  ;  then   echo yes  ;  fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== tabs instead of spaces
if	true;	then	echo yes;	fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


=== mixed whitespace
if 	 true 	 ; 	 then 	 echo yes 	 ; 	 fi
---
(if (command (word "true")) (command (word "echo") (word "yes")))
---


# === From tree-sitter-bash ===

=== if with command substitution condition
if test "$(whoami)" = root; then echo root; fi
---
(if (command (word "test") (word "\"$(whoami)\"") (word "=") (word "root")) (command (word "echo") (word "root")))
---


=== if elif else complete
if [ -f a ]; then echo a; elif [ -f b ]; then echo b; else echo none; fi
---
(if (command (word "[") (word "-f") (word "a") (word "]")) (command (word "echo") (word "a")) (if (command (word "[") (word "-f") (word "b") (word "]")) (command (word "echo") (word "b")) (command (word "echo") (word "none"))))
---


=== if with variable assignment condition
if foo=1; then echo set; fi
---
(if (command (word "foo=1")) (command (word "echo") (word "set")))
---


=== if with result variable simple
if result=value; then echo got; fi
---
(if (command (word "result=value")) (command (word "echo") (word "got")))
---


# === Special test cases ===

=== test with equals in value
if [ a = -d ]; then echo match; fi
---
(if (command (word "[") (word "a") (word "=") (word "-d") (word "]")) (command (word "echo") (word "match")))
---


=== bracket test with spaces in quotes
if [ "hello world" = "hello world" ]; then echo same; fi
---
(if (command (word "[") (word "\"hello world\"") (word "=") (word "\"hello world\"") (word "]")) (command (word "echo") (word "same")))
---


=== test with empty string
if [ -z "" ]; then echo empty; fi
---
(if (command (word "[") (word "-z") (word "\"\"") (word "]")) (command (word "echo") (word "empty")))
---


=== test with non-empty string
if [ -n "foo" ]; then echo nonempty; fi
---
(if (command (word "[") (word "-n") (word "\"foo\"") (word "]")) (command (word "echo") (word "nonempty")))
---


# === Null command in body ===

=== empty then with colon
if true; then :; fi
---
(if (command (word "true")) (command (word ":")))
---


=== empty then with colon and else
if false; then :; else echo no; fi
---
(if (command (word "false")) (command (word ":")) (command (word "echo") (word "no")))
---


=== colon in condition
if :; then echo always; fi
---
(if (command (word ":")) (command (word "echo") (word "always")))
---


# === If with redirections in body ===

=== redirect in then body
if true; then echo yes > out.txt; fi
---
(if (command (word "true")) (command (word "echo") (word "yes") (redirect ">" "out.txt")))
---


=== redirect in else body
if false; then echo yes; else echo no > err.txt; fi
---
(if (command (word "false")) (command (word "echo") (word "yes")) (command (word "echo") (word "no") (redirect ">" "err.txt")))
---


=== multiple redirects in body
if true; then cat < in.txt > out.txt; fi
---
(if (command (word "true")) (command (word "cat") (redirect "<" "in.txt") (redirect ">" "out.txt")))
---


=== pipeline with redirects in body
if true; then cat file | grep foo > matches.txt; fi
---
(if (command (word "true")) (pipe (command (word "cat") (word "file")) (command (word "grep") (word "foo") (redirect ">" "matches.txt"))))
---


# === Complex real-world examples ===

=== git status check
if git status > /dev/null; then echo repo; fi
---
(if (command (word "git") (word "status") (redirect ">" "/dev/null")) (command (word "echo") (word "repo")))
---


=== command existence check
if command -v git > /dev/null; then echo found; fi
---
(if (command (word "command") (word "-v") (word "git") (redirect ">" "/dev/null")) (command (word "echo") (word "found")))
---


=== file readable and not empty
if [ -r "$file" ] && [ -s "$file" ]; then cat "$file"; fi
---
(if (and (command (word "[") (word "-r") (word "\"$file\"") (word "]")) (command (word "[") (word "-s") (word "\"$file\"") (word "]"))) (command (word "cat") (word "\"$file\"")))
---


=== complex elif with pipelines
if cat a | grep x; then echo ax; elif cat b | grep x; then echo bx; elif cat c | grep x; then echo cx; else echo none; fi
---
(if (pipe (command (word "cat") (word "a")) (command (word "grep") (word "x"))) (command (word "echo") (word "ax")) (if (pipe (command (word "cat") (word "b")) (command (word "grep") (word "x"))) (command (word "echo") (word "bx")) (if (pipe (command (word "cat") (word "c")) (command (word "grep") (word "x"))) (command (word "echo") (word "cx")) (command (word "echo") (word "none")))))
---


=== if in subshell
(if true; then echo yes; fi)
---
(subshell (if (command (word "true")) (command (word "echo") (word "yes"))))
---


=== if in brace group
{ if true; then echo yes; fi; }
---
(brace-group (if (command (word "true")) (command (word "echo") (word "yes"))))
---


=== chained if statements with and
if a; then echo a; fi && if b; then echo b; fi
---
(and (if (command (word "a")) (command (word "echo") (word "a"))) (if (command (word "b")) (command (word "echo") (word "b"))))
---


=== if with here string condition
if cat <<< "test"; then echo yes; fi
---
(if (command (word "cat") (redirect "<<<" ""test"")) (command (word "echo") (word "yes")))
---