rable 0.2.0

A Rust implementation of the Parable bash parser — complete GNU Bash 5.3-compatible parsing with Python bindings
Documentation
# Oracle-derived tests: bash valid divergences
# Inputs where bash-oracle parses successfully but rable
# produces a different AST (or errors). Generated by
# differential fuzzing — tests/fuzz.py mutate --valid-only
#
# 31 test cases across 7 tracked issues:
#   #35 - rbracket_outside_cond     (]] tokenization outside [[ ]])
#   #36 - bracket_op_split          (unbalanced [...] absorbing || / &&)
#   #37 - reserved_word_as_word     (reserved words in non-reserved positions)
#   #38 - backtick_opaque           (invalid backtick body -> error)
#   #39 - heredoc_in_cmdsub         (heredoc inside $(...))
#   #40 - cmdsub_reformat           (command-sub canonical reformat drift)
#   #41 - linecont_in_arith         (backslash-newline in $(( )))

# --- ]] tokenization outside [[ ]] (A_rbracket_word_split) ---

=== rbracket_outside_cond 1
][[ "$file" == *.txt ]]
---
(command (word "][[") (word "\"$file\"") (word "==") (word "*.txt") (word "]]"))
---

=== rbracket_outside_cond 2
[c[ $x =~ ]+[a-z] ]]
---
(command (word "[c[") (word "$x") (word "=~") (word "]+[a-z]") (word "]]"))
---

=== rbracket_outside_cond 3
Cdeclare -n ref=t ]] arget
---
(command (word "Cdeclare") (word "-n") (word "ref=t") (word "]]") (word "arget"))
---

# --- word boundary inside unbalanced [...] (B_bracket_word_splits_on_ops) ---

=== bracket_op_split 1
echo ho $$[a||b]
---
(or (command (word "echo") (word "ho") (word "$$[a")) (command (word "b]")))
---

=== bracket_op_split 2
Decho $ case[a&&b]
---
(and (command (word "Decho") (word "$") (word "case[a")) (command (word "b]")))
---

=== bracket_op_split 3
foo^[a-echo ${foo^[a-z]}
---
(command (word "foo^[a-echo") (word "${foo^[a-z]}"))
---

# --- reserved words as plain words (C_rable_errors_on_valid) ---

=== reserved_word_as_word 1
if foo= for $(pwd); then foo=$(pwd); fi
---
(if (command (word "foo=") (word "for") (word "$(pwd)")) (command (word "foo=$(pwd)")))
---

=== reserved_word_as_word 2
((x 
> 0)
)
---
(subshell (subshell (semi (command (word "x")) (command (redirect ">" "0")))))
---

=== reserved_word_as_word 3
while arr[0]=$fo do o; do arr[0]=$fo do o; done
---
(while (command (word "arr[0]=$fo") (word "do") (word "o")) (command (word "arr[0]=$fo") (word "do") (word "o")))
---

=== reserved_word_as_word 4
case $x in a) x=$ then [1+2];; esac
---
(case (word "$x") (pattern ((word "a")) (command (word "x=$") (word "then") (word "[1+2]"))))
---

=== reserved_word_as_word 5
if [ $"yes" = yes9 ][ $; then echo ok; fi
---
(if (command (word "[") (word "\"yes\"") (word "=") (word "yes9") (word "][") (word "$")) (command (word "echo") (word "ok")))
---

# --- backticks as opaque words when content is invalid (C_backtick_opaque) ---

=== backtick_opaque 1
echo "`if true then echo a; fi` until "
---
(command (word "echo") (word "\"`if true then echo a; fi` until \""))
---

=== backtick_opaque 2
echo "`i<f true; then echo a; fi`"
---
(command (word "echo") (word "\"`i<f true; then echo a; fi`\""))
---

=== backtick_opaque 3
x=`fo<>r i in a b; do echo $i; if  done`
---
(command (word "x=`fo<>r i in a b; do echo $i; if  done`"))
---

=== backtick_opaque 4
x=`echo $(case y|| in (a) z;; esac)`
---
(command (word "x=`echo $(case y|| in (a) z;; esac)`"))
---

=== backtick_opaque 5
e else cho ` else echo "hello"`
---
(command (word "e") (word "else") (word "cho") (word "` else echo \"hello\"`"))
---

=== backtick_opaque 6
$( x=`tr G$'\'n` )
---
(command (word "$(x=`tr G$'\\'n`)"))
---

# --- heredoc inside $( ... ) with unusual content (C_heredoc_unterminated) ---

=== heredoc_in_cmdsub 1
x=$(cat <<\EOF
(unclosed
d
EOF
EOF
)
---
(command (word "x=$(cat <<'EOF'\n(unclosed\nd\nEOF\n\nEOF)"))
---

=== heredoc_in_cmdsub 2
$( ( cat <<EOF >>o fi utput.txt
content
EOF ) )
---
(command (word "$( ( cat fi utput.txt <<EOF >> o\ncontent\nEOF\n ))"))
---

# --- command-substitution canonical reformat drift (D_cmdsub_format_drift) ---

=== cmdsub_reformat 1
$( exec {fd}>file & )
---
(command (word "$(exec {fd}> file &)"))
---

=== cmdsub_reformat 2
$( select x in a b; do cat; done <<< inp5ut )
---
(command (word "$(select x in a b;\ndo\n    cat;\ndone <<< inp5ut)"))
---

=== cmdsub_reformat 3
$( select x in a b c; do if [ "$x" = "db"]; then break; fi; done )
---
(command (word "$(select x in a b c;\ndo\n    if [ \"$x\" = \"db\"]; then\n        break;\n    fi;\ndone)"))
---

=== cmdsub_reformat 4
$( function f ( echo Pi ) )
---
(command (word "$(function f () \n{ \n    ( echo Pi )\n})"))
---

=== cmdsub_reformat 5
echo $(f() {  hi; echo hi; }; f)
---
(command (word "echo") (word "$(function f () \n{ \n    hi;\n    echo hi\n}; f)"))
---

# --- heredoc quoted-delimiter marker preserved in reformat (E_heredoc_quote_marker) ---

=== cmdsub_reformat 6
x=$(Tcat<<\EOF
(unclosed
EOF
)
---
(command (word "x=$(Tcat <<'EOF'\n(unclosed\nEOF\n)"))
---

# --- backslash-newline not processed inside $(( )) (F_linecont_in_arith) ---

=== linecont_in_arith 1
echo $;((1 + \
2))
---
(semi (command (word "echo") (word "$")) (arith (word "1 + 2")))
---

=== linecont_in_arith 2
((1 + \
2))
---
(arith (word "1 + 2"))
---

=== linecont_in_arith 3
echo $((1 + \
2))
---
(command (word "echo") (word "$((1 + 2))"))
---

=== linecont_in_arith 4
echo $((va\
r + 1))
---
(command (word "echo") (word "$((var + 1))"))
---

=== linecont_in_arith 5
((va\
r + 1))
---
(arith (word "var + 1"))
---

=== linecont_in_arith 6
for ((i=0;\
i<3;\
i++)); do :; done
---
(arith-for (init (word "i=0")) (test (word "i<3")) (step (word "i++")) (command (word ":")))
---