[Python-checkins] gh-98692: Enable treating shebang lines as executables in py.exe launcher (GH-98732)
miss-islington
webhook-mailer at python.org
Mon Oct 31 17:31:32 EDT 2022
https://github.com/python/cpython/commit/46a3cf4fe3380b5d4560589cce8f602ba949832d
commit: 46a3cf4fe3380b5d4560589cce8f602ba949832d
branch: 3.11
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2022-10-31T14:31:26-07:00
summary:
gh-98692: Enable treating shebang lines as executables in py.exe launcher (GH-98732)
(cherry picked from commit 88297e2a8a75898228360ee369628a4a6111e2ee)
Co-authored-by: Steve Dower <steve.dower at python.org>
files:
A Misc/NEWS.d/next/Windows/2022-10-26-17-43-09.gh-issue-98692.bOopfZ.rst
M Doc/using/windows.rst
M Lib/test/test_launcher.py
M PC/launcher2.c
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
index 4ab68e140b9e..4526dc34872d 100644
--- a/Doc/using/windows.rst
+++ b/Doc/using/windows.rst
@@ -853,7 +853,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
``-V:<TAG>`` argument with the complete tag.
-
The ``/usr/bin/env`` form of shebang line has one further special property.
Before looking for installed Python interpreters, this form will search the
executable :envvar:`PATH` for a Python executable. This corresponds to the
@@ -863,6 +862,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
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
--------------------------
diff --git a/Lib/test/test_launcher.py b/Lib/test/test_launcher.py
index ba6856b3e246..be6d0022693b 100644
--- a/Lib/test/test_launcher.py
+++ b/Lib/test/test_launcher.py
@@ -517,6 +517,14 @@ def test_py_shebang(self):
self.assertEqual("3.100", data["SearchInfo.tag"])
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):
with self.py_ini(TEST_PY_COMMANDS):
with self.script("#! /usr/bin/python2 -prearg") as script:
@@ -618,3 +626,42 @@ def test_install(self):
self.assertIn("winget.exe", cmd)
# Both command lines include the store ID
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(),
+ )
diff --git a/Misc/NEWS.d/next/Windows/2022-10-26-17-43-09.gh-issue-98692.bOopfZ.rst b/Misc/NEWS.d/next/Windows/2022-10-26-17-43-09.gh-issue-98692.bOopfZ.rst
new file mode 100644
index 000000000000..3a5efd9a1cfa
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2022-10-26-17-43-09.gh-issue-98692.bOopfZ.rst
@@ -0,0 +1,2 @@
+Fix the :ref:`launcher` ignoring unrecognized shebang lines instead of
+treating them as local paths
diff --git a/PC/launcher2.c b/PC/launcher2.c
index b1ad5f066ede..5bcd2ba8a067 100644
--- a/PC/launcher2.c
+++ b/PC/launcher2.c
@@ -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
checkShebang(SearchInfo *search)
{
@@ -963,13 +1019,19 @@ checkShebang(SearchInfo *search)
L"/usr/bin/env ",
L"/usr/bin/",
L"/usr/local/bin/",
- L"",
+ L"python",
NULL
};
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
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])) {
commandLength += 1;
}
@@ -1012,11 +1074,14 @@ checkShebang(SearchInfo *search)
debug(L"# Found shebang command but could not execute it: %.*s\n",
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);
}
More information about the Python-checkins
mailing list