Skip to content

Commit 31128f0

Browse files
authored
Merge pull request #524 from nerdvegas/windows_fixes
Windows fixes
2 parents 9e5f447 + a5d4abb commit 31128f0

File tree

4 files changed

+120
-17
lines changed

4 files changed

+120
-17
lines changed

src/rez/shells.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
Pluggable API for creating subshells using different programs, such as bash.
33
"""
44
from rez.rex import RexExecutor, ActionInterpreter, OutputStyle
5-
from rez.util import which, shlex_join
5+
from rez.util import shlex_join
6+
from rez.backport.shutilwhich import which
67
from rez.utils.logging_ import print_warning
78
from rez.utils.system import popen
89
from rez.system import system
910
from rez.exceptions import RezSystemError
1011
from rez.rex import EscapedString
1112
from rez.config import config
1213
import subprocess
14+
import os
1315
import os.path
1416
import pipes
1517

@@ -58,14 +60,6 @@ def startup_capabilities(cls, rcfile=False, norc=False, stdin=False,
5860
"""
5961
raise NotImplementedError
6062

61-
#@cached_class_property
62-
#def executable(cls):
63-
# name = cls.name()
64-
# exe = which(name)
65-
# if not exe:
66-
# raise RuntimeError("Couldn't find executable '%s'." % name)
67-
# return exe
68-
6963
@classmethod
7064
def get_syspaths(cls):
7165
raise NotImplementedError
@@ -107,8 +101,25 @@ def _overruled_option(cls, option, overruling_option, val):
107101
% (option, cls.name(), overruling_option))
108102

109103
@classmethod
110-
def find_executable(cls, name):
104+
def find_executable(cls, name, check_syspaths=False):
105+
"""Find an executable.
106+
107+
Args:
108+
name (str): Program name.
109+
check_syspaths (bool): If True, check the standard system paths as
110+
well, if program was not found on current $PATH.
111+
112+
Returns:
113+
str: Full filepath of executable.
114+
"""
111115
exe = which(name)
116+
117+
if not exe and check_syspaths:
118+
paths = cls.get_syspaths()
119+
env = os.environ.copy()
120+
env["PATH"] = os.pathsep.join(paths)
121+
exe = which(name, env=env)
122+
112123
if not exe:
113124
raise RuntimeError("Couldn't find executable '%s'." % name)
114125
return exe

src/rez/tests/test_shells.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,11 @@ def _rex_assigning():
197197

198198
def _print(value):
199199
env.FOO = value
200-
info("%FOO%" if windows else "${FOO}")
200+
# Wrap the output in quotes to prevent the shell from
201+
# interpreting parts of our output as commands. This can happen
202+
# when we include special characters (&, <, >, ^) in a
203+
# variable.
204+
info('"%FOO%"' if windows else '"${FOO}"')
201205

202206
env.GREET = "hi"
203207
env.WHO = "Gary"
@@ -226,6 +230,12 @@ def _print(value):
226230
_print(literal("${WHO}"))
227231
_print(literal("${WHO}").e(" %WHO%" if windows else " $WHO"))
228232

233+
# Make sure we are escaping &, <, >, ^ properly.
234+
_print('hey & world')
235+
_print('hey > world')
236+
_print('hey < world')
237+
_print('hey ^ world')
238+
229239
expected_output = [
230240
"ello",
231241
"ello",
@@ -249,9 +259,18 @@ def _print(value):
249259
"hi Gary",
250260
"hi $WHO",
251261
"${WHO}",
252-
"${WHO} Gary"
262+
"${WHO} Gary",
263+
"hey & world",
264+
"hey > world",
265+
"hey < world",
266+
"hey ^ world"
253267
]
254268

269+
# We are wrapping all variable outputs in quotes in order to make sure
270+
# our shell isn't interpreting our output as instructions when echoing
271+
# it but this means we need to wrap our expected output as well.
272+
expected_output = ['"{}"'.format(o) for o in expected_output]
273+
255274
_execute_code(_rex_assigning, expected_output)
256275

257276
def _rex_appending():
@@ -273,6 +292,40 @@ def _rex_appending():
273292

274293
_execute_code(_rex_appending, expected_output)
275294

295+
@shell_dependent()
296+
def test_rex_code_alias(self):
297+
"""Ensure PATH changes do not influence the alias command.
298+
299+
This is important for Windows because the doskey.exe might not be on
300+
the PATH anymore at the time it's executed. That's why we figure out
301+
the absolute path to doskey.exe before we modify PATH and continue to
302+
use the absolute path after the modifications.
303+
304+
"""
305+
def _execute_code(func):
306+
loc = inspect.getsourcelines(func)[0][1:]
307+
code = textwrap.dedent('\n'.join(loc))
308+
r = self._create_context([])
309+
p = r.execute_rex_code(code, stdout=subprocess.PIPE)
310+
311+
out, _ = p.communicate()
312+
self.assertEqual(p.returncode, 0)
313+
314+
def _alias_after_path_manipulation():
315+
# Appending something to the PATH and creating an alias afterwards
316+
# did fail before we implemented a doskey specific fix.
317+
env.PATH.append("hey")
318+
alias('alias_test', '"echo test_echo"')
319+
320+
# We can not run the command from a batch file because the Windows
321+
# doskey doesn't support it. From the docs:
322+
# "You cannot run a doskey macro from a batch program."
323+
# command('alias_test')
324+
325+
# We don't expect any output, the shell should just return with exit
326+
# code 0.
327+
_execute_code(_alias_after_path_manipulation)
328+
276329

277330
if __name__ == '__main__':
278331
unittest.main()

src/rez/utils/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22

33
# Update this value to version up Rez. Do not place anything else in this file.
4-
_rez_version = "2.20.0"
4+
_rez_version = "2.20.1"
55

66
try:
77
from rez.vendor.version.version import Version

src/rezplugins/shell/cmd.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
Windows Command Prompt (DOS) shell.
33
"""
44
from rez.config import config
5-
from rez.rex import RexExecutor, literal, OutputStyle
5+
from rez.rex import RexExecutor, literal, OutputStyle, EscapedString
66
from rez.shells import Shell
77
from rez.system import system
88
from rez.utils.system import popen
99
from rez.utils.platform_ import platform_
1010
from rez.util import shlex_join
11+
from functools import partial
1112
import os
1213
import re
1314
import subprocess
@@ -19,6 +20,12 @@ class CMD(Shell):
1920
# http://ss64.com/nt/cmd.html
2021
syspaths = None
2122
_executable = None
23+
_doskey = None
24+
25+
# Regex to aid with escaping of Windows-specific special chars:
26+
# http://ss64.com/nt/syntax-esc.html
27+
_escape_re = re.compile(r'(?<!\^)[&<>]|(?<!\^)\^(?![&<>\^])')
28+
_escaper = partial(_escape_re.sub, lambda m: '^' + m.group(0))
2229

2330
@property
2431
def executable(cls):
@@ -182,8 +189,20 @@ def _create_ex():
182189
_record_shell(executor, files=startup_sequence["files"], print_msg=(not quiet))
183190

184191
if shell_command:
192+
# Launch the provided command in the configured shell and wait
193+
# until it exits.
185194
executor.command(shell_command)
186-
executor.command('exit %errorlevel%')
195+
196+
# Test for None specifically because resolved_context.execute_rex_code
197+
# passes '' and we do NOT want to keep a shell open during a rex code
198+
# exec operation.
199+
elif shell_command is None:
200+
# Launch the configured shell itself and wait for user interaction
201+
# to exit.
202+
executor.command('cmd /Q /K')
203+
204+
# Exit the configured shell.
205+
executor.command('exit %errorlevel%')
187206

188207
code = executor.get_output()
189208
target_file = os.path.join(tmpdir, "rez-shell.%s"
@@ -226,7 +245,18 @@ def get_output(self, style=OutputStyle.file):
226245
return script
227246

228247
def escape_string(self, value):
229-
return value
248+
"""Escape the <, >, ^, and & special characters reserved by Windows.
249+
250+
Args:
251+
value (str/EscapedString): String or already escaped string.
252+
253+
Returns:
254+
str: The value escaped for Windows.
255+
256+
"""
257+
if isinstance(value, EscapedString):
258+
return value.formatted(self._escaper)
259+
return self._escaper(value)
230260

231261
def _saferefenv(self, key):
232262
pass
@@ -245,7 +275,16 @@ def resetenv(self, key, value, friends=None):
245275
self._addline(self.setenv(key, value))
246276

247277
def alias(self, key, value):
248-
self._addline("doskey %s=%s" % (key, value))
278+
# find doskey, falling back to system paths if not in $PATH. Fall back
279+
# to unqualified 'doskey' if all else fails
280+
if self._doskey is None:
281+
try:
282+
self.__class__._doskey = \
283+
self.find_executable("doskey", check_syspaths=True)
284+
except:
285+
self._doskey = "doskey"
286+
287+
self._addline("%s %s=%s" % (self._doskey, key, value))
249288

250289
def comment(self, value):
251290
for line in value.split('\n'):

0 commit comments

Comments
 (0)