Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-98692: Enable treating shebang lines as executables in py.exe launcher #98732

Merged
merged 7 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Doc/using/windows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
``-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
Expand All @@ -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
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
--------------------------
Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,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:
Expand Down Expand Up @@ -617,3 +625,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(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix the :ref:`launcher` ignoring unrecognized shebang lines instead of
treating them as local paths
71 changes: 68 additions & 3 deletions PC/launcher2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
zooba marked this conversation as resolved.
Show resolved Hide resolved

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)
{
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}


Expand Down