# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project
"""
Windows Command Prompt (DOS) shell.
"""
from rez.config import config
from rez.rex import RexExecutor, expandable, OutputStyle, EscapedString
from rez.shells import Shell
from rez.system import system
from rez.utils.execution import Popen
from rez.utils.platform_ import platform_
from rez.vendor.six import six
from ._utils.windows import to_windows_path, get_syspaths_from_registry
from functools import partial
import os
import re
import subprocess
basestring = six.string_types[0]
[docs]class CMD(Shell):
# For reference, the ss64 web page provides useful documentation on builtin
# commands for the Windows Command Prompt (cmd). It can be found here :
# http://ss64.com/nt/cmd.html
syspaths = None
_doskey = None
expand_env_vars = True
_env_var_regex = re.compile("%([A-Za-z0-9_]+)%") # %ENVVAR%
# Regex to aid with escaping of Windows-specific special chars:
# http://ss64.com/nt/syntax-esc.html
_escape_re = re.compile(r'(?<!\^)[&<>]|(?<!\^)\^(?![&<>\^])|(\|)')
_escaper = partial(_escape_re.sub, lambda m: '^' + m.group(0))
def __init__(self):
super(CMD, self).__init__()
self._doskey_aliases = {}
[docs] @classmethod
def name(cls):
return 'cmd'
[docs] @classmethod
def file_extension(cls):
return 'bat'
[docs] @classmethod
def startup_capabilities(cls, rcfile=False, norc=False, stdin=False,
command=False):
cls._unsupported_option('rcfile', rcfile)
rcfile = False
cls._unsupported_option('norc', norc)
norc = False
cls._unsupported_option('stdin', stdin)
stdin = False
return (rcfile, norc, stdin, command)
[docs] @classmethod
def get_startup_sequence(cls, rcfile, norc, stdin, command):
rcfile, norc, stdin, command = \
cls.startup_capabilities(rcfile, norc, stdin, command)
return dict(
stdin=stdin,
command=command,
do_rcfile=False,
envvar=None,
files=[],
bind_files=[],
source_bind_files=(not norc)
)
[docs] @classmethod
def get_syspaths(cls):
if cls.syspaths is not None:
return cls.syspaths
if config.standard_system_paths:
cls.syspaths = config.standard_system_paths
return cls.syspaths
cls.syspaths = get_syspaths_from_registry()
return cls.syspaths
def _bind_interactive_rez(self):
if config.set_prompt and self.settings.prompt:
stored_prompt = os.getenv("REZ_STORED_PROMPT_CMD")
curr_prompt = stored_prompt or os.getenv("PROMPT", "")
if not stored_prompt:
self.setenv("REZ_STORED_PROMPT_CMD", curr_prompt)
new_prompt = "%%REZ_ENV_PROMPT%%"
new_prompt = (new_prompt + " %s") if config.prefix_prompt \
else ("%s " + new_prompt)
new_prompt = new_prompt % curr_prompt
self._addline('set PROMPT=%s' % new_prompt)
[docs] def spawn_shell(self, context_file, tmpdir, rcfile=None, norc=False,
stdin=False, command=None, env=None, quiet=False,
pre_command=None, add_rez=True, **Popen_args):
command = self._expand_alias(command)
startup_sequence = self.get_startup_sequence(rcfile, norc, bool(stdin), command)
shell_command = None
def _record_shell(ex, files, bind_rez=True, print_msg=False):
ex.source(context_file)
if startup_sequence["envvar"]:
ex.unsetenv(startup_sequence["envvar"])
if add_rez and bind_rez:
ex.interpreter._bind_interactive_rez()
if print_msg and add_rez and not quiet:
ex.info('')
ex.info('You are now in a rez-configured environment.')
ex.info('')
if system.is_production_rez_install:
# previously this was called with the /K flag, however
# that would leave spawn_shell hung on a blocked call
# waiting for the user to type "exit" into the shell that
# was spawned to run the rez context printout
ex.command("cmd /Q /C rez context")
def _create_ex():
return RexExecutor(interpreter=self.new_shell(),
parent_environ={},
add_default_namespaces=False)
executor = _create_ex()
if self.settings.prompt:
executor.interpreter._saferefenv('REZ_ENV_PROMPT')
executor.env.REZ_ENV_PROMPT = \
expandable("%REZ_ENV_PROMPT%").literal(self.settings.prompt)
# Make .py launch within cmd without extension.
if self.settings.additional_pathext:
# Ensure that the PATHEXT does not append duplicates.
fmt = (
'echo %PATHEXT%|C:\\Windows\\System32\\findstr.exe /i /c:"{0}">nul '
'|| set PATHEXT=%PATHEXT%;{0}'
)
for pathext in self.settings.additional_pathext:
executor.command(fmt.format(pathext))
# This resets the errorcode, which is tainted by the code above
executor.command("(call )")
if startup_sequence["command"] is not None:
_record_shell(executor, files=startup_sequence["files"])
shell_command = startup_sequence["command"]
else:
_record_shell(executor, files=startup_sequence["files"], print_msg=(not quiet))
if shell_command:
# Launch the provided command in the configured shell and wait
# until it exits.
executor.command(shell_command)
# Test for None specifically because resolved_context.execute_rex_code
# passes '' and we do NOT want to keep a shell open during a rex code
# exec operation.
elif shell_command is None:
# Launch the configured shell itself and wait for user interaction
# to exit.
executor.command('cmd /Q /K')
# Exit the configured shell.
executor.command('exit %errorlevel%')
code = executor.get_output()
target_file = os.path.join(tmpdir, "rez-shell.%s"
% self.file_extension())
with open(target_file, 'w') as f:
f.write(code)
if startup_sequence["stdin"] and stdin and (stdin is not True):
Popen_args["stdin"] = stdin
cmd = []
if pre_command:
if isinstance(pre_command, basestring):
cmd = pre_command.strip().split()
else:
cmd = pre_command
# Test for None specifically because resolved_context.execute_rex_code
# passes '' and we do NOT want to keep a shell open during a rex code
# exec operation.
if shell_command is None:
cmd_flags = ['/Q', '/K']
else:
cmd_flags = ['/Q', '/C']
cmd += [self.executable]
cmd += cmd_flags
cmd += ['call {}'.format(target_file)]
is_detached = (cmd[0] == 'START')
p = Popen(cmd, env=env, shell=is_detached, **Popen_args)
return p
[docs] def get_output(self, style=OutputStyle.file):
if style == OutputStyle.file:
script = '\n'.join(self._lines) + '\n'
else: # eval style
lines = []
for line in self._lines:
if not line.startswith('REM'): # strip comments
line = line.rstrip()
lines.append(line)
script = '&& '.join(lines)
return script
[docs] def escape_string(self, value, is_path=False):
"""Escape the <, >, ^, and & special characters reserved by Windows.
Args:
value (str/EscapedString): String or already escaped string.
Returns:
str: The value escaped for Windows.
"""
value = EscapedString.promote(value)
value = value.expanduser()
result = ''
for is_literal, txt in value.strings:
if is_literal:
txt = self._escaper(txt)
# Note that cmd uses ^% while batch files use %% to escape %
txt = self._env_var_regex.sub(r"%%\1%%", txt)
else:
if is_path:
txt = self.normalize_paths(txt)
txt = self._escaper(txt)
result += txt
return result
[docs] def normalize_path(self, path):
return to_windows_path(path)
def _saferefenv(self, key):
pass
[docs] def shebang(self):
pass
[docs] def setenv(self, key, value):
value = self.escape_string(value, is_path=self._is_pathed_key(key))
self._addline('set %s=%s' % (key, value))
[docs] def unsetenv(self, key):
self._addline("set %s=" % key)
[docs] def resetenv(self, key, value, friends=None):
self._addline(self.setenv(key, value))
[docs] def alias(self, key, value):
# find doskey, falling back to system paths if not in $PATH. Fall back
# to unqualified 'doskey' if all else fails
if self._doskey is None:
try:
self.__class__._doskey = \
self.find_executable("doskey", check_syspaths=True)
except:
self._doskey = "doskey"
self._doskey_aliases[key] = value
self._addline("%s %s=%s $*" % (self._doskey, key, value))
[docs] def info(self, value):
for line in value.split('\n'):
line = self.escape_string(line)
line = self.convert_tokens(line)
if line:
self._addline('echo %s' % line)
else:
self._addline('echo.')
[docs] def error(self, value):
for line in value.split('\n'):
line = self.escape_string(line)
line = self.convert_tokens(line)
self._addline('echo "%s" 1>&2' % line)
[docs] def source(self, value):
self._addline("call %s" % value)
[docs] def command(self, value):
self._addline(value)
[docs] @classmethod
def get_all_key_tokens(cls, key):
return ["%{}%".format(key)]
[docs] @classmethod
def join(cls, command):
# TODO: This needs to be properly fixed, see other shell impls
# at https://github.com/nerdvegas/rez/pull/1130
#
# TODO: This may disappear in future [1]
# [1] https://bugs.python.org/issue10838
return subprocess.list2cmdline(command)
[docs] @classmethod
def line_terminator(cls):
return "\r\n"
def _expand_alias(self, command):
"""Expand `command` if alias is being presented
This is important for Windows CMD shell because the doskey.exe isn't
executed yet when the alias is being passed in `command`. This means we
cannot rely on doskey.exe to execute alias in first run. So here we
lookup alias that were just parsed from package, replace it with full
command if matched.
"""
if command:
word = command.split()[0]
resolved_alias = self._doskey_aliases.get(word)
if resolved_alias:
command = command.replace(word, resolved_alias, 1)
return command
[docs]def register_plugin():
if platform_.name == "windows":
return CMD