(P: ParseState, kwTok: Token)
| 3644 | } |
| 3645 | |
| 3646 | function parseUnset(P: ParseState, kwTok: Token): TsNode { |
| 3647 | const kw = leaf(P, 'unset', kwTok) |
| 3648 | const kids: TsNode[] = [kw] |
| 3649 | while (true) { |
| 3650 | skipBlanks(P.L) |
| 3651 | const c = peek(P.L) |
| 3652 | if ( |
| 3653 | c === '' || |
| 3654 | c === '\n' || |
| 3655 | c === ';' || |
| 3656 | c === '&' || |
| 3657 | c === '|' || |
| 3658 | c === ')' || |
| 3659 | c === '<' || |
| 3660 | c === '>' |
| 3661 | ) { |
| 3662 | break |
| 3663 | } |
| 3664 | // SECURITY: use parseWord (not raw nextToken) so quoted strings like |
| 3665 | // `unset 'a[$(id)]'` emit a raw_string child that ast.ts can reject. |
| 3666 | // Previously `break` silently dropped non-WORD args — hiding the |
| 3667 | // arithmetic-subscript code-exec vector from the security walker. |
| 3668 | const arg = parseWord(P, 'arg') |
| 3669 | if (!arg) break |
| 3670 | if (arg.type === 'word') { |
| 3671 | if (arg.text.startsWith('-')) { |
| 3672 | kids.push(arg) |
| 3673 | } else { |
| 3674 | kids.push(mk(P, 'variable_name', arg.startIndex, arg.endIndex, [])) |
| 3675 | } |
| 3676 | } else { |
| 3677 | kids.push(arg) |
| 3678 | } |
| 3679 | } |
| 3680 | const last = kids[kids.length - 1]! |
| 3681 | return mk(P, 'unset_command', kw.startIndex, last.endIndex, kids) |
| 3682 | } |
| 3683 | |
| 3684 | function consumeKeyword(P: ParseState, name: string, kids: TsNode[]): void { |
| 3685 | skipNewlines(P) |
no test coverage detected