(P: ParseState, closer: string)
| 3819 | } |
| 3820 | |
| 3821 | function parseTestBinary(P: ParseState, closer: string): TsNode | null { |
| 3822 | skipBlanks(P.L) |
| 3823 | // `!` in test context binds tighter than =~/==. |
| 3824 | // `[[ ! "x" =~ y ]]` → (binary_expression (unary_expression (string)) (regex)) |
| 3825 | // `[[ ! -f x ]]` → (unary_expression ! (unary_expression (test_operator) (word))) |
| 3826 | const left = parseTestNegatablePrimary(P, closer) |
| 3827 | if (!left) return null |
| 3828 | skipBlanks(P.L) |
| 3829 | // Binary comparison: == != =~ -eq -lt etc. |
| 3830 | const c = peek(P.L) |
| 3831 | const c1 = peek(P.L, 1) |
| 3832 | let op: TsNode | null = null |
| 3833 | const os = P.L.b |
| 3834 | if (c === '=' && c1 === '=') { |
| 3835 | advance(P.L) |
| 3836 | advance(P.L) |
| 3837 | op = mk(P, '==', os, P.L.b, []) |
| 3838 | } else if (c === '!' && c1 === '=') { |
| 3839 | advance(P.L) |
| 3840 | advance(P.L) |
| 3841 | op = mk(P, '!=', os, P.L.b, []) |
| 3842 | } else if (c === '=' && c1 === '~') { |
| 3843 | advance(P.L) |
| 3844 | advance(P.L) |
| 3845 | op = mk(P, '=~', os, P.L.b, []) |
| 3846 | } else if (c === '=' && c1 !== '=') { |
| 3847 | advance(P.L) |
| 3848 | op = mk(P, '=', os, P.L.b, []) |
| 3849 | } else if (c === '<' && c1 !== '<') { |
| 3850 | advance(P.L) |
| 3851 | op = mk(P, '<', os, P.L.b, []) |
| 3852 | } else if (c === '>' && c1 !== '>') { |
| 3853 | advance(P.L) |
| 3854 | op = mk(P, '>', os, P.L.b, []) |
| 3855 | } else if (c === '-' && isIdentStart(c1)) { |
| 3856 | advance(P.L) |
| 3857 | while (isIdentChar(peek(P.L))) advance(P.L) |
| 3858 | op = mk(P, 'test_operator', os, P.L.b, []) |
| 3859 | } |
| 3860 | if (!op) return left |
| 3861 | skipBlanks(P.L) |
| 3862 | // In [[ ]], RHS of ==/!=/=/=~ gets special pattern parsing: paren counting |
| 3863 | // so @(a|b|c) doesn't break on |, and segments become extglob_pattern/regex. |
| 3864 | if (closer === ']]') { |
| 3865 | const opText = op.type |
| 3866 | if (opText === '=~') { |
| 3867 | skipBlanks(P.L) |
| 3868 | // If the ENTIRE RHS is a quoted string, emit string/raw_string not |
| 3869 | // regex: `[[ "$x" =~ "$y" ]]` → (binary_expression (string) (string)). |
| 3870 | // If there's content after the quote (`' boop '(.*)$`), the whole RHS |
| 3871 | // stays a single (regex). Peek past the quote to check. |
| 3872 | const rc = peek(P.L) |
| 3873 | let rhs: TsNode | null = null |
| 3874 | if (rc === '"' || rc === "'") { |
| 3875 | const save = saveLex(P.L) |
| 3876 | const quoted = |
| 3877 | rc === '"' |
| 3878 | ? parseDoubleQuoted(P) |
no test coverage detected