* A `redirected_statement` wraps a command (or pipeline) plus one or more * `file_redirect`/`heredoc_redirect` nodes. Extract redirects, walk the * inner command, attach redirects to the LAST command (the one whose output * is being redirected).
( node: Node, commands: SimpleCommand[], varScope: Map<string, string>, )
| 1015 | * is being redirected). |
| 1016 | */ |
| 1017 | function walkRedirectedStatement( |
| 1018 | node: Node, |
| 1019 | commands: SimpleCommand[], |
| 1020 | varScope: Map<string, string>, |
| 1021 | ): ParseForSecurityResult | null { |
| 1022 | const redirects: Redirect[] = [] |
| 1023 | let innerCommand: Node | null = null |
| 1024 | |
| 1025 | for (const child of node.children) { |
| 1026 | if (!child) continue |
| 1027 | if (child.type === 'file_redirect') { |
| 1028 | // Thread `commands` so $() in redirect targets (e.g., `> $(mktemp)`) |
| 1029 | // extracts the inner command for permission checking. |
| 1030 | const r = walkFileRedirect(child, commands, varScope) |
| 1031 | if ('kind' in r) return r |
| 1032 | redirects.push(r) |
| 1033 | } else if (child.type === 'heredoc_redirect') { |
| 1034 | const r = walkHeredocRedirect(child) |
| 1035 | if (r) return r |
| 1036 | } else if ( |
| 1037 | child.type === 'command' || |
| 1038 | child.type === 'pipeline' || |
| 1039 | child.type === 'list' || |
| 1040 | child.type === 'negated_command' || |
| 1041 | child.type === 'declaration_command' || |
| 1042 | child.type === 'unset_command' |
| 1043 | ) { |
| 1044 | innerCommand = child |
| 1045 | } else { |
| 1046 | return tooComplex(child) |
| 1047 | } |
| 1048 | } |
| 1049 | |
| 1050 | if (!innerCommand) { |
| 1051 | // `> file` alone is valid bash (truncates file). Represent as a command |
| 1052 | // with empty argv so downstream sees the write. |
| 1053 | commands.push({ argv: [], envVars: [], redirects, text: node.text }) |
| 1054 | return null |
| 1055 | } |
| 1056 | |
| 1057 | const before = commands.length |
| 1058 | const err = collectCommands(innerCommand, commands, varScope) |
| 1059 | if (err) return err |
| 1060 | if (commands.length > before && redirects.length > 0) { |
| 1061 | const last = commands[commands.length - 1] |
| 1062 | if (last) last.redirects.push(...redirects) |
| 1063 | } |
| 1064 | return null |
| 1065 | } |
| 1066 | |
| 1067 | /** |
| 1068 | * Extract operator + target from a `file_redirect` node. The target must be |
no test coverage detected