(
part: ParseEntry,
prev: ParseEntry | undefined,
next: ParseEntry | undefined,
nextNext: ParseEntry | undefined,
nextNextNext: ParseEntry | undefined,
redirections: Array<{ target: string; operator: '>' | '>>' }>,
kept: ParseEntry[],
)
| 858 | } |
| 859 | |
| 860 | function handleRedirection( |
| 861 | part: ParseEntry, |
| 862 | prev: ParseEntry | undefined, |
| 863 | next: ParseEntry | undefined, |
| 864 | nextNext: ParseEntry | undefined, |
| 865 | nextNextNext: ParseEntry | undefined, |
| 866 | redirections: Array<{ target: string; operator: '>' | '>>' }>, |
| 867 | kept: ParseEntry[], |
| 868 | ): { skip: number; dangerous: boolean } { |
| 869 | const isFileDescriptor = (p: ParseEntry | undefined): p is string => |
| 870 | typeof p === 'string' && /^\d+$/.test(p.trim()) |
| 871 | |
| 872 | // Handle > and >> operators |
| 873 | if (isOperator(part, '>') || isOperator(part, '>>')) { |
| 874 | const operator = (part as { op: '>' | '>>' }).op |
| 875 | |
| 876 | // File descriptor redirection (2>, 3>, etc.) |
| 877 | if (isFileDescriptor(prev)) { |
| 878 | // Check for ZSH force clobber syntax (2>! file, 2>>! file) |
| 879 | if (next === '!' && isSimpleTarget(nextNext)) { |
| 880 | return handleFileDescriptorRedirection( |
| 881 | prev.trim(), |
| 882 | operator, |
| 883 | nextNext, // Skip the "!" and use the actual target |
| 884 | redirections, |
| 885 | kept, |
| 886 | 2, // Skip both "!" and the target |
| 887 | ) |
| 888 | } |
| 889 | // 2>! with dangerous expansion target |
| 890 | if (next === '!' && hasDangerousExpansion(nextNext)) { |
| 891 | return { skip: 0, dangerous: true } |
| 892 | } |
| 893 | // Check for POSIX force overwrite syntax (2>| file, 2>>| file) |
| 894 | if (isOperator(next, '|') && isSimpleTarget(nextNext)) { |
| 895 | return handleFileDescriptorRedirection( |
| 896 | prev.trim(), |
| 897 | operator, |
| 898 | nextNext, // Skip the "|" and use the actual target |
| 899 | redirections, |
| 900 | kept, |
| 901 | 2, // Skip both "|" and the target |
| 902 | ) |
| 903 | } |
| 904 | // 2>| with dangerous expansion target |
| 905 | if (isOperator(next, '|') && hasDangerousExpansion(nextNext)) { |
| 906 | return { skip: 0, dangerous: true } |
| 907 | } |
| 908 | // 2>!filename (no space) - shell-quote parses as 2 > "!filename". |
| 909 | // In Zsh, 2>! is force clobber and the remainder undergoes expansion, |
| 910 | // e.g., 2>!=rg expands to 2>! /usr/bin/rg, 2>!~root/.bashrc expands to |
| 911 | // 2>! /var/root/.bashrc. We must strip the ! and check for dangerous |
| 912 | // expansion in the remainder. Mirrors the non-FD handler below. |
| 913 | // Exclude history expansion patterns (!!, !-n, !?, !digit). |
| 914 | if ( |
| 915 | typeof next === 'string' && |
| 916 | next.startsWith('!') && |
| 917 | next.length > 1 && |
no test coverage detected