Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 46a3cf4

Browse files
gh-98692: Enable treating shebang lines as executables in py.exe launcher (GH-98732)
(cherry picked from commit 88297e2) Co-authored-by: Steve Dower <[email protected]>
1 parent 2b0cbb9 commit 46a3cf4

File tree

4 files changed

+124
-4
lines changed

4 files changed

+124
-4
lines changed

Doc/using/windows.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,6 @@ minor version. I.e. ``/usr/bin/python3.7-32`` will request usage of the
853853
not provably i386/32-bit". To request a specific environment, use the new
854854
``-V:<TAG>`` argument with the complete tag.
855855

856-
857856
The ``/usr/bin/env`` form of shebang line has one further special property.
858857
Before looking for installed Python interpreters, this form will search the
859858
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
863862
variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
864863
this additional search.
865864

865+
Shebang lines that do not match any of these patterns are treated as **Windows**
866+
paths that are absolute or relative to the directory containing the script file.
867+
This is a convenience for Windows-only scripts, such as those generated by an
868+
installer, since the behavior is not compatible with Unix-style shells.
869+
These paths may be quoted, and may include multiple arguments, after which the
870+
path to the script and any additional arguments will be appended.
871+
866872

867873
Arguments in shebang lines
868874
--------------------------

Lib/test/test_launcher.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,14 @@ def test_py_shebang(self):
517517
self.assertEqual("3.100", data["SearchInfo.tag"])
518518
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
519519

520+
def test_python_shebang(self):
521+
with self.py_ini(TEST_PY_COMMANDS):
522+
with self.script("#! python -prearg") as script:
523+
data = self.run_py([script, "-postarg"])
524+
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
525+
self.assertEqual("3.100", data["SearchInfo.tag"])
526+
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
527+
520528
def test_py2_shebang(self):
521529
with self.py_ini(TEST_PY_COMMANDS):
522530
with self.script("#! /usr/bin/python2 -prearg") as script:
@@ -618,3 +626,42 @@ def test_install(self):
618626
self.assertIn("winget.exe", cmd)
619627
# Both command lines include the store ID
620628
self.assertIn("9PJPW5LDXLZ5", cmd)
629+
630+
def test_literal_shebang_absolute(self):
631+
with self.script(f"#! C:/some_random_app -witharg") as script:
632+
data = self.run_py([script])
633+
self.assertEqual(
634+
f"C:\\some_random_app -witharg {script}",
635+
data["stdout"].strip(),
636+
)
637+
638+
def test_literal_shebang_relative(self):
639+
with self.script(f"#! ..\\some_random_app -witharg") as script:
640+
data = self.run_py([script])
641+
self.assertEqual(
642+
f"{script.parent.parent}\\some_random_app -witharg {script}",
643+
data["stdout"].strip(),
644+
)
645+
646+
def test_literal_shebang_quoted(self):
647+
with self.script(f'#! "some random app" -witharg') as script:
648+
data = self.run_py([script])
649+
self.assertEqual(
650+
f'"{script.parent}\\some random app" -witharg {script}',
651+
data["stdout"].strip(),
652+
)
653+
654+
with self.script(f'#! some" random "app -witharg') as script:
655+
data = self.run_py([script])
656+
self.assertEqual(
657+
f'"{script.parent}\\some random app" -witharg {script}',
658+
data["stdout"].strip(),
659+
)
660+
661+
def test_literal_shebang_quoted_escape(self):
662+
with self.script(f'#! some\\" random "app -witharg') as script:
663+
data = self.run_py([script])
664+
self.assertEqual(
665+
f'"{script.parent}\\some\\ random app" -witharg {script}',
666+
data["stdout"].strip(),
667+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix the :ref:`launcher` ignoring unrecognized shebang lines instead of
2+
treating them as local paths

PC/launcher2.c

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,62 @@ _findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
871871
}
872872

873873

874+
int
875+
_useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength)
876+
{
877+
wchar_t buffer[MAXLEN];
878+
wchar_t script[MAXLEN];
879+
wchar_t command[MAXLEN];
880+
881+
int commandLength = 0;
882+
int inQuote = 0;
883+
884+
if (!shebang || !shebangLength) {
885+
return 0;
886+
}
887+
888+
wchar_t *pC = command;
889+
for (int i = 0; i < shebangLength; ++i) {
890+
wchar_t c = shebang[i];
891+
if (isspace(c) && !inQuote) {
892+
commandLength = i;
893+
break;
894+
} else if (c == L'"') {
895+
inQuote = !inQuote;
896+
} else if (c == L'/' || c == L'\\') {
897+
*pC++ = L'\\';
898+
} else {
899+
*pC++ = c;
900+
}
901+
}
902+
*pC = L'\0';
903+
904+
if (!GetCurrentDirectoryW(MAXLEN, buffer) ||
905+
wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) ||
906+
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script,
907+
PATHCCH_ALLOW_LONG_PATHS)) ||
908+
FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) ||
909+
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command,
910+
PATHCCH_ALLOW_LONG_PATHS))
911+
) {
912+
return RC_NO_MEMORY;
913+
}
914+
915+
int n = (int)wcsnlen(buffer, MAXLEN);
916+
wchar_t *path = allocSearchInfoBuffer(search, n + 1);
917+
if (!path) {
918+
return RC_NO_MEMORY;
919+
}
920+
wcscpy_s(path, n + 1, buffer);
921+
search->executablePath = path;
922+
if (commandLength) {
923+
search->executableArgs = &shebang[commandLength];
924+
search->executableArgsLength = shebangLength - commandLength;
925+
}
926+
return 0;
927+
}
928+
929+
874930
int
875931
checkShebang(SearchInfo *search)
876932
{
@@ -963,13 +1019,19 @@ checkShebang(SearchInfo *search)
9631019
L"/usr/bin/env ",
9641020
L"/usr/bin/",
9651021
L"/usr/local/bin/",
966-
L"",
1022+
L"python",
9671023
NULL
9681024
};
9691025

9701026
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
9711027
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
9721028
commandLength = 0;
1029+
// Normally "python" is the start of the command, but we also need it
1030+
// as a shebang prefix for back-compat. We move the command marker back
1031+
// if we match on that one.
1032+
if (0 == wcscmp(*tmpl, L"python")) {
1033+
command -= 6;
1034+
}
9731035
while (command[commandLength] && !isspace(command[commandLength])) {
9741036
commandLength += 1;
9751037
}
@@ -1012,11 +1074,14 @@ checkShebang(SearchInfo *search)
10121074
debug(L"# Found shebang command but could not execute it: %.*s\n",
10131075
commandLength, command);
10141076
}
1015-
break;
1077+
// search is done by this point
1078+
return 0;
10161079
}
10171080
}
10181081

1019-
return 0;
1082+
// Unrecognised commands are joined to the script's directory and treated
1083+
// as the executable path
1084+
return _useShebangAsExecutable(search, shebang, shebangLength);
10201085
}
10211086

10221087

0 commit comments

Comments
 (0)