* Parse a redirect operator + destination(s). * @param greedy When true, file_redirect consumes repeat1($._literal) per * grammar's prec.left — `cmd >f a b c` attaches `a b c` to the redirect. * When false (preRedirect context), takes only 1 destination because * command's dynamic preceden
(P: ParseState, greedy = false)
| 1621 | * command's dynamic precedence beats redirected_statement's prec(-1). |
| 1622 | */ |
| 1623 | function tryParseRedirect(P: ParseState, greedy = false): TsNode | null { |
| 1624 | const save = saveLex(P.L) |
| 1625 | skipBlanks(P.L) |
| 1626 | // File descriptor prefix? |
| 1627 | let fd: TsNode | null = null |
| 1628 | if (isDigit(peek(P.L))) { |
| 1629 | const startB = P.L.b |
| 1630 | let j = P.L.i |
| 1631 | while (j < P.L.len && isDigit(P.L.src[j]!)) j++ |
| 1632 | const after = j < P.L.len ? P.L.src[j]! : '' |
| 1633 | if (after === '>' || after === '<') { |
| 1634 | while (P.L.i < j) advance(P.L) |
| 1635 | fd = mk(P, 'file_descriptor', startB, P.L.b, []) |
| 1636 | } |
| 1637 | } |
| 1638 | const t = nextToken(P.L, 'arg') |
| 1639 | if (t.type !== 'OP') { |
| 1640 | restoreLex(P.L, save) |
| 1641 | return null |
| 1642 | } |
| 1643 | const v = t.value |
| 1644 | if (v === '<<<') { |
| 1645 | const op = leaf(P, '<<<', t) |
| 1646 | skipBlanks(P.L) |
| 1647 | const target = parseWord(P, 'arg') |
| 1648 | const end = target ? target.endIndex : op.endIndex |
| 1649 | const kids = target ? [op, target] : [op] |
| 1650 | return mk( |
| 1651 | P, |
| 1652 | 'herestring_redirect', |
| 1653 | fd ? fd.startIndex : op.startIndex, |
| 1654 | end, |
| 1655 | fd ? [fd, ...kids] : kids, |
| 1656 | ) |
| 1657 | } |
| 1658 | if (v === '<<' || v === '<<-') { |
| 1659 | const op = leaf(P, v, t) |
| 1660 | // Heredoc start — delimiter word (may be quoted) |
| 1661 | skipBlanks(P.L) |
| 1662 | const dStart = P.L.b |
| 1663 | let quoted = false |
| 1664 | let delim = '' |
| 1665 | const dc = peek(P.L) |
| 1666 | if (dc === "'" || dc === '"') { |
| 1667 | quoted = true |
| 1668 | advance(P.L) |
| 1669 | while (P.L.i < P.L.len && peek(P.L) !== dc) { |
| 1670 | delim += peek(P.L) |
| 1671 | advance(P.L) |
| 1672 | } |
| 1673 | if (P.L.i < P.L.len) advance(P.L) |
| 1674 | } else if (dc === '\\') { |
| 1675 | // Backslash-escaped delimiter: \X — exactly one escaped char, body is |
| 1676 | // quoted (literal). Covers <<\EOF <<\' <<\\ etc. |
| 1677 | quoted = true |
| 1678 | advance(P.L) |
| 1679 | if (P.L.i < P.L.len && peek(P.L) !== '\n') { |
| 1680 | delim += peek(P.L) |
no test coverage detected