1+ """This module has functions for looking into scripts to decide how to launch.
2+
3+ Currently, this is primarily shebang lines. This support is intended to allow
4+ scripts to be somewhat portable between POSIX (where they are natively handled)
5+ and Windows, when launching in Python. They are not intended to provide generic
6+ shebang support, although for historical/compatibility reasons it is possible.
7+
8+ Shebang commands shaped like '/usr/bin/<command>' or '/usr/local/bin/<command>'
9+ will have the command matched to an alias or executable name for detected
10+ runtimes, with the first match being selected.
11+ A command of 'py', 'pyw', 'python' or 'pythonw' will match the default runtime.
12+ If the install manager has been launched in windowed mode, and the selected
13+ alias is not marked as windowed, then the first windowed 'run-for' target will
14+ be substituted (if present - otherwise, it will just not run windowed). Aliases
15+ that map to windowed targets are launched windowed.
16+ If no matching command is found, the default install will be used.
17+
18+ Shebang commands shaped like '/usr/bin/env <command>' will do the same lookup as
19+ above. If no matching command is found, the current PATH environment variable
20+ will be searched for a matching command. It will be launched with a warning,
21+ configuration permitting.
22+
23+ Other shebangs will be treated directly as the command, doing the same lookup
24+ and the same PATH search.
25+
26+ It is not yet implemented, but this is also where a search for PEP 723 inline
27+ script metadata would go. Find the comment mentioning PEP 723 below.
28+ """
29+
130import re
231
332from .logging import LOGGER
@@ -12,7 +41,7 @@ class NoShebang(Exception):
1241 pass
1342
1443
15- def _find_shebang_command (cmd , full_cmd ):
44+ def _find_shebang_command (cmd , full_cmd , * , windowed = None ):
1645 sh_cmd = PurePath (full_cmd )
1746 # HACK: Assuming alias/executable suffix is '.exe' here
1847 # (But correctly assuming we can't use with_suffix() or .stem)
@@ -22,17 +51,32 @@ def _find_shebang_command(cmd, full_cmd):
2251 is_wdefault = sh_cmd .match ("pythonw.exe" ) or sh_cmd .match ("pyw.exe" )
2352 is_default = is_wdefault or sh_cmd .match ("python.exe" ) or sh_cmd .match ("py.exe" )
2453
54+ # Internal logic error, but non-fatal, if it has no value
55+ assert windowed is not None
56+
57+ # Ensure we use the default install for a default name. Otherwise, a
58+ # "higher" runtime may claim it via an alias, which is not the intent.
59+ if is_default :
60+ for i in cmd .get_installs ():
61+ if i .get ("default" ):
62+ exe = i ["executable" ]
63+ if is_wdefault or windowed :
64+ target = [t for t in i .get ("run-for" , []) if t .get ("windowed" )]
65+ if target :
66+ exe = target [0 ]["target" ]
67+ return {** i , "executable" : i ["prefix" ] / exe }
68+
2569 for i in cmd .get_installs ():
26- if is_default and i .get ("default" ):
27- if is_wdefault :
28- target = [t for t in i .get ("run-for" , []) if t .get ("windowed" )]
29- if target :
30- return {** i , "executable" : i ["prefix" ] / target [0 ]["target" ]}
31- return {** i , "executable" : i ["prefix" ] / i ["executable" ]}
3270 for a in i .get ("alias" , ()):
3371 if sh_cmd .match (a ["name" ]):
72+ exe = a ["target" ]
3473 LOGGER .debug ("Matched alias %s in %s" , a ["name" ], i ["id" ])
35- return {** i , "executable" : i ["prefix" ] / a ["target" ]}
74+ if windowed and not a .get ("windowed" ):
75+ target = [t for t in i .get ("run-for" , []) if t .get ("windowed" )]
76+ if target :
77+ exe = target [0 ]["target" ]
78+ LOGGER .debug ("Substituting target %s for windowed=1" , exe )
79+ return {** i , "executable" : i ["prefix" ] / exe }
3680 if sh_cmd .full_match (PurePath (i ["executable" ]).name ):
3781 LOGGER .debug ("Matched executable name %s in %s" , i ["executable" ], i ["id" ])
3882 return i
@@ -69,15 +113,15 @@ def _find_on_path(cmd, full_cmd):
69113 }
70114
71115
72- def _parse_shebang (cmd , line ):
116+ def _parse_shebang (cmd , line , * , windowed = None ):
73117 # For /usr[/local]/bin, we look for a matching alias name.
74118 shebang = re .match (r"#!\s*/usr/(?:local/)?bin/(?!env\b)([^\\/\s]+).*" , line )
75119 if shebang :
76120 # Handle the /usr[/local]/bin/python cases
77121 full_cmd = shebang .group (1 )
78122 LOGGER .debug ("Matching shebang: %s" , full_cmd )
79123 try :
80- return _find_shebang_command (cmd , full_cmd )
124+ return _find_shebang_command (cmd , full_cmd , windowed = windowed )
81125 except LookupError :
82126 LOGGER .warn ("A shebang '%s' was found, but could not be matched "
83127 "to an installed runtime." , full_cmd )
@@ -93,7 +137,7 @@ def _parse_shebang(cmd, line):
93137 # First do regular install lookup for /usr/bin/env shebangs
94138 full_cmd = shebang .group (1 )
95139 try :
96- return _find_shebang_command (cmd , full_cmd )
140+ return _find_shebang_command (cmd , full_cmd , windowed = windowed )
97141 except LookupError :
98142 pass
99143 # If not, warn and do regular PATH search
@@ -107,7 +151,7 @@ def _parse_shebang(cmd, line):
107151 "Python runtimes, set 'shebang_can_run_anything' to "
108152 "'false' in your configuration file." )
109153 return i
110-
154+
111155 else :
112156 LOGGER .warn ("A shebang '%s' was found, but could not be matched "
113157 "to an installed runtime." , full_cmd )
@@ -125,7 +169,7 @@ def _parse_shebang(cmd, line):
125169 # A regular lookup will handle the case where the entire shebang is
126170 # a valid alias.
127171 try :
128- return _find_shebang_command (cmd , full_cmd )
172+ return _find_shebang_command (cmd , full_cmd , windowed = windowed )
129173 except LookupError :
130174 pass
131175 if cmd .shebang_can_run_anything or cmd .shebang_can_run_anything_silently :
@@ -149,7 +193,7 @@ def _parse_shebang(cmd, line):
149193 raise NoShebang
150194
151195
152- def _read_script (cmd , script , encoding ):
196+ def _read_script (cmd , script , encoding , * , windowed = None ):
153197 try :
154198 f = open (script , "r" , encoding = encoding , errors = "replace" )
155199 except OSError as ex :
@@ -158,7 +202,7 @@ def _read_script(cmd, script, encoding):
158202 first_line = next (f , "" ).rstrip ()
159203 if first_line .startswith ("#!" ):
160204 try :
161- return _parse_shebang (cmd , first_line )
205+ return _parse_shebang (cmd , first_line , windowed = windowed )
162206 except LookupError :
163207 raise LookupError (script ) from None
164208 except NoShebang :
@@ -168,20 +212,20 @@ def _read_script(cmd, script, encoding):
168212 if coding and coding .group (1 ) != encoding :
169213 raise NewEncoding (coding .group (1 ))
170214
171- # TODO: Parse inline script metadata
215+ # TODO: Parse inline script metadata (PEP 723)
172216 # This involves finding '# /// script' followed by
173217 # a line with '# requires-python = <spec>'.
174218 # That spec needs to be processed as a version constraint, which
175219 # cmd.get_install_to_run() can handle.
176220 raise LookupError (script )
177221
178222
179- def find_install_from_script (cmd , script ):
223+ def find_install_from_script (cmd , script , * , windowed = False ):
180224 try :
181- return _read_script (cmd , script , "utf-8-sig" )
225+ return _read_script (cmd , script , "utf-8-sig" , windowed = windowed )
182226 except NewEncoding as ex :
183227 encoding = ex .args [0 ]
184- return _read_script (cmd , script , encoding )
228+ return _read_script (cmd , script , encoding , windowed = windowed )
185229
186230
187231def _maybe_quote (a ):
0 commit comments