diff --git a/scripts/hooks/block-no-verify.js b/scripts/hooks/block-no-verify.js index 4189565fa..1d19e5c35 100644 --- a/scripts/hooks/block-no-verify.js +++ b/scripts/hooks/block-no-verify.js @@ -649,7 +649,14 @@ function sedScriptParts(words, budget) { while (i < words.length) { const raw = words[i].value; budget.spend(raw.length + 1); - if (raw === '--') break; // remaining operands are files (data) + if (raw === '--') { + // Options end here, but the operands do not: with no script option + // given, the first operand after -- is still the script (GNU sed 4.9 + // verified). Keep collecting them. + i++; + while (i < words.length) { positionals.push(words[i].value); budget.spend(words[i].value.length + 1); i++; } + break; + } if (raw === '--expression') { if (!words[i + 1]) return null; scripts.push(words[i + 1].value); i += 2; continue; diff --git a/tests/hooks/block-no-verify.test.js b/tests/hooks/block-no-verify.test.js index 4f040ce07..56549e721 100644 --- a/tests/hooks/block-no-verify.test.js +++ b/tests/hooks/block-no-verify.test.js @@ -2172,6 +2172,10 @@ const sedCases = [ ['later -e after a positional script operand is scanned', "sed 's/x/y/' -e 'e git commit --no-verify'", 2], ['later --expression= after positionals is scanned', "sed 's/x/y/' --expression='s/x/git commit --no-verify/e'", 2], ['-- ends options: later tokens are file operands', "sed -e 's/x/y/' -- 'git commit --no-verify'", 0], + // With no script option before --, the first operand after -- is still the + // script (Greptile P1 on this PR, GNU sed 4.9 verified). + ['first operand after -- is still the script', "printf x | sed -- 's/x/git commit --no-verify/e'", 2], + ['file operands after the -- script stay data', "sed -- 's/x/y/' 'git commit --no-verify'", 0], // GNU sed decodes escapes in exec-bearing fragments before execution // (Greptile P1 findings on this PR): decoding that surface faithfully is out // of scope, so escaped fragments are conservatively blocked instead.