fix(highlight): env -S payload is a full arg list, not a bare interpreter

The attached split-string payload (`-Spython3`, `--split-string=...`) can
itself begin with env options or VAR=value assignments before the
interpreter: `-S-i python3`, `-SFOO=bar python3`,
`--split-string=-u FOO python3`. Rather than taking the payload's first
word as the interpreter, re-inject the attached payload into the token
stream so it flows through the same option / operand / assignment state
machine as a separated payload. Adds the three cases as resolver tests.
This commit is contained in:
Levi Neuwirth 2026-07-14 16:00:47 +01:00
parent 558d00020f
commit 7479213c3f
2 changed files with 21 additions and 10 deletions

View File

@ -98,13 +98,21 @@ function pmacs.parse.language_from_shebang(buf)
base = nil
local seen_env = false
local skip_next = false
local tokens = {}
for tok in rest:gmatch("%S+") do
-- `-S`/`--split-string` introduces a string whose FIRST word is the
-- interpreter. GNU env accepts that value ATTACHED — `-Spython3`,
-- `-vSpython3` (after no-operand short flags i/v/0), or
-- `--split-string=python3` — where the interpreter rides inside the
-- option token. Extract it; the separated forms (`-S python3`) are
-- handled by simply walking on to the next token.
tokens[#tokens + 1] = tok
end
local i = 1
while i <= #tokens do
local tok = tokens[i]
i = i + 1
-- `-S`/`--split-string` introduces a complete env argument list,
-- not necessarily an interpreter first: it may begin with more env
-- options or `VAR=value` assignments. GNU env accepts that value
-- ATTACHED — `-Spython3`, `-vSpython3` (after no-operand short flags
-- i/v/0), or `--split-string=python3`. Put an attached payload back
-- into this token stream so it goes through the same option/operand/
-- assignment state machine as a separated payload.
local split_attached =
tok:match("^%-[iv0]*S(.+)$") or tok:match("^%-%-split%-string=(.+)$")
if not seen_env then
@ -112,12 +120,10 @@ function pmacs.parse.language_from_shebang(buf)
elseif skip_next then
skip_next = false -- the operand consumed by the previous option
elseif split_attached then
local word = split_attached:match("^(%S+)")
base = word and (word:match("([^/]+)$") or word)
break
table.insert(tokens, i, split_attached)
elseif tok == "-S" or tok == "--split-string" then
-- Separated split-string: the next token starts the string, i.e.
-- the interpreter — keep walking.
-- another env argument — keep walking.
elseif tok:find("=", 1, true) then
-- `VAR=value` env assignment, or another `--long=value` option:
-- self-contained, skip.

View File

@ -5883,6 +5883,11 @@ fn m4_shebang_resolver_maps_interpreters() {
("#!/usr/bin/env -Spython3 -u\n", "python"),
("#!/usr/bin/env --split-string=python3 -u\n", "python"),
("#!/usr/bin/env -vSpython3 -u\n", "python"),
// The attached split string is a complete env argument list, so
// options and assignments may precede the interpreter within it.
("#!/usr/bin/env -S-i python3 -u\n", "python"),
("#!/usr/bin/env -SFOO=bar python3 -u\n", "python"),
("#!/usr/bin/env --split-string=-u FOO python3\n", "python"),
// GNU-env options that consume an operand must not have the
// operand mistaken for the interpreter.
("#!/usr/bin/env -u FOO python3\n", "python"),