mirror of https://github.com/python/cpython
gh-98692: Enable treating shebang lines as executables in py.exe launcher (GH-98732)
This commit is contained in:
parent
4702552885
commit
88297e2a8a
|
@ -866,7 +866,6 @@ minor version. I.e. ``/usr/bin/python3.7-32`` will request usage of the
|
||||||
not provably i386/32-bit". To request a specific environment, use the new
|
not provably i386/32-bit". To request a specific environment, use the new
|
||||||
``-V:<TAG>`` argument with the complete tag.
|
``-V:<TAG>`` argument with the complete tag.
|
||||||
|
|
||||||
|
|
||||||
The ``/usr/bin/env`` form of shebang line has one further special property.
|
The ``/usr/bin/env`` form of shebang line has one further special property.
|
||||||
Before looking for installed Python interpreters, this form will search the
|
Before looking for installed Python interpreters, this form will search the
|
||||||
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
||||||
|
@ -876,6 +875,13 @@ be found, it will be handled as described below. Additionally, the environment
|
||||||
variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
|
variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
|
||||||
this additional search.
|
this additional search.
|
||||||
|
|
||||||
|
Shebang lines that do not match any of these patterns are treated as **Windows**
|
||||||
|
paths that are absolute or relative to the directory containing the script file.
|
||||||
|
This is a convenience for Windows-only scripts, such as those generated by an
|
||||||
|
installer, since the behavior is not compatible with Unix-style shells.
|
||||||
|
These paths may be quoted, and may include multiple arguments, after which the
|
||||||
|
path to the script and any additional arguments will be appended.
|
||||||
|
|
||||||
|
|
||||||
Arguments in shebang lines
|
Arguments in shebang lines
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
|
@ -516,6 +516,14 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
self.assertEqual("3.100", data["SearchInfo.tag"])
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
|
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_python_shebang(self):
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script("#! python -prearg") as script:
|
||||||
|
data = self.run_py([script, "-postarg"])
|
||||||
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
|
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
def test_py2_shebang(self):
|
def test_py2_shebang(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/python2 -prearg") as script:
|
with self.script("#! /usr/bin/python2 -prearg") as script:
|
||||||
|
@ -617,3 +625,42 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
self.assertIn("winget.exe", cmd)
|
self.assertIn("winget.exe", cmd)
|
||||||
# Both command lines include the store ID
|
# Both command lines include the store ID
|
||||||
self.assertIn("9PJPW5LDXLZ5", cmd)
|
self.assertIn("9PJPW5LDXLZ5", cmd)
|
||||||
|
|
||||||
|
def test_literal_shebang_absolute(self):
|
||||||
|
with self.script(f"#! C:/some_random_app -witharg") as script:
|
||||||
|
data = self.run_py([script])
|
||||||
|
self.assertEqual(
|
||||||
|
f"C:\\some_random_app -witharg {script}",
|
||||||
|
data["stdout"].strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_literal_shebang_relative(self):
|
||||||
|
with self.script(f"#! ..\\some_random_app -witharg") as script:
|
||||||
|
data = self.run_py([script])
|
||||||
|
self.assertEqual(
|
||||||
|
f"{script.parent.parent}\\some_random_app -witharg {script}",
|
||||||
|
data["stdout"].strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_literal_shebang_quoted(self):
|
||||||
|
with self.script(f'#! "some random app" -witharg') as script:
|
||||||
|
data = self.run_py([script])
|
||||||
|
self.assertEqual(
|
||||||
|
f'"{script.parent}\\some random app" -witharg {script}',
|
||||||
|
data["stdout"].strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.script(f'#! some" random "app -witharg') as script:
|
||||||
|
data = self.run_py([script])
|
||||||
|
self.assertEqual(
|
||||||
|
f'"{script.parent}\\some random app" -witharg {script}',
|
||||||
|
data["stdout"].strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_literal_shebang_quoted_escape(self):
|
||||||
|
with self.script(f'#! some\\" random "app -witharg') as script:
|
||||||
|
data = self.run_py([script])
|
||||||
|
self.assertEqual(
|
||||||
|
f'"{script.parent}\\some\\ random app" -witharg {script}',
|
||||||
|
data["stdout"].strip(),
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix the :ref:`launcher` ignoring unrecognized shebang lines instead of
|
||||||
|
treating them as local paths
|
|
@ -871,6 +871,62 @@ _findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
_useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength)
|
||||||
|
{
|
||||||
|
wchar_t buffer[MAXLEN];
|
||||||
|
wchar_t script[MAXLEN];
|
||||||
|
wchar_t command[MAXLEN];
|
||||||
|
|
||||||
|
int commandLength = 0;
|
||||||
|
int inQuote = 0;
|
||||||
|
|
||||||
|
if (!shebang || !shebangLength) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t *pC = command;
|
||||||
|
for (int i = 0; i < shebangLength; ++i) {
|
||||||
|
wchar_t c = shebang[i];
|
||||||
|
if (isspace(c) && !inQuote) {
|
||||||
|
commandLength = i;
|
||||||
|
break;
|
||||||
|
} else if (c == L'"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
} else if (c == L'/' || c == L'\\') {
|
||||||
|
*pC++ = L'\\';
|
||||||
|
} else {
|
||||||
|
*pC++ = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*pC = L'\0';
|
||||||
|
|
||||||
|
if (!GetCurrentDirectoryW(MAXLEN, buffer) ||
|
||||||
|
wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) ||
|
||||||
|
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script,
|
||||||
|
PATHCCH_ALLOW_LONG_PATHS)) ||
|
||||||
|
FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) ||
|
||||||
|
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command,
|
||||||
|
PATHCCH_ALLOW_LONG_PATHS))
|
||||||
|
) {
|
||||||
|
return RC_NO_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = (int)wcsnlen(buffer, MAXLEN);
|
||||||
|
wchar_t *path = allocSearchInfoBuffer(search, n + 1);
|
||||||
|
if (!path) {
|
||||||
|
return RC_NO_MEMORY;
|
||||||
|
}
|
||||||
|
wcscpy_s(path, n + 1, buffer);
|
||||||
|
search->executablePath = path;
|
||||||
|
if (commandLength) {
|
||||||
|
search->executableArgs = &shebang[commandLength];
|
||||||
|
search->executableArgsLength = shebangLength - commandLength;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
checkShebang(SearchInfo *search)
|
checkShebang(SearchInfo *search)
|
||||||
{
|
{
|
||||||
|
@ -963,13 +1019,19 @@ checkShebang(SearchInfo *search)
|
||||||
L"/usr/bin/env ",
|
L"/usr/bin/env ",
|
||||||
L"/usr/bin/",
|
L"/usr/bin/",
|
||||||
L"/usr/local/bin/",
|
L"/usr/local/bin/",
|
||||||
L"",
|
L"python",
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
|
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
|
||||||
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
|
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
|
||||||
commandLength = 0;
|
commandLength = 0;
|
||||||
|
// Normally "python" is the start of the command, but we also need it
|
||||||
|
// as a shebang prefix for back-compat. We move the command marker back
|
||||||
|
// if we match on that one.
|
||||||
|
if (0 == wcscmp(*tmpl, L"python")) {
|
||||||
|
command -= 6;
|
||||||
|
}
|
||||||
while (command[commandLength] && !isspace(command[commandLength])) {
|
while (command[commandLength] && !isspace(command[commandLength])) {
|
||||||
commandLength += 1;
|
commandLength += 1;
|
||||||
}
|
}
|
||||||
|
@ -1012,11 +1074,14 @@ checkShebang(SearchInfo *search)
|
||||||
debug(L"# Found shebang command but could not execute it: %.*s\n",
|
debug(L"# Found shebang command but could not execute it: %.*s\n",
|
||||||
commandLength, command);
|
commandLength, command);
|
||||||
}
|
}
|
||||||
break;
|
// search is done by this point
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
// Unrecognised commands are joined to the script's directory and treated
|
||||||
|
// as the executable path
|
||||||
|
return _useShebangAsExecutable(search, shebang, shebangLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue