Skip to content

Commit

Permalink
Fixes AcademySoftwareFoundation#694 and lets cmd and powershell pass …
Browse files Browse the repository at this point in the history
…all shell tests.

This implements ${VAR} and $VAR variables for cmd and Powershell like, as
well as their native forms like %VAR% and $Env:VAR.
In order to handle the ambiguity of variables in the form of $Env:Literal
in Unix and Windows the NamespaceFormatter may take interpreter regex into
account that is being supplied by the underlying shell.

For command execution on Windows .PY is being added to the PATHEXT and
the create_script function is extended to create any form of execution
script. This behaviour can be controlled via a rezconfig, but defaults to
backwards compatible Unix-only behaviour.
  • Loading branch information
bfloch committed Aug 26, 2019
1 parent c4cbd37 commit 9a27edf
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 357 deletions.
19 changes: 13 additions & 6 deletions src/rez/bind/hello_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rez.package_maker__ import make_package
from rez.vendor.version.version import Version
from rez.utils.lint_helper import env
from rez.util import create_executable_script
from rez.util import create_executable_script, ExecutableScriptMode
from rez.bind._utils import make_dirs, check_version
import os.path

Expand All @@ -26,10 +26,10 @@ def hello_world_source():

p = OptionParser()
p.add_option("-q", dest="quiet", action="store_true",
help="quiet mode")
help="quiet mode")
p.add_option("-r", dest="retcode", type="int", default=0,
help="exit with a non-zero return code")
opts,args = p.parse_args()
help="exit with a non-zero return code")
opts, args = p.parse_args()

if not opts.quiet:
print("Hello Rez World!")
Expand All @@ -43,7 +43,15 @@ def bind(path, version_range=None, opts=None, parser=None):
def make_root(variant, root):
binpath = make_dirs(root, "bin")
filepath = os.path.join(binpath, "hello_world")
create_executable_script(filepath, hello_world_source)

# In order for this script to run on each platform we create the
# platform-specific script. This also requires the additional_pathext
# setting of the windows shell plugins to include ".PY"
create_executable_script(
filepath,
hello_world_source,
py_script_mode=ExecutableScriptMode.platform_specific
)

with make_package("hello_world", path, make_root=make_root) as pkg:
pkg.version = version
Expand All @@ -52,7 +60,6 @@ def make_root(variant, root):

return pkg.installed_variants


# Copyright 2013-2016 Allan Johns.
#
# This library is free software: you can redistribute it and/or
Expand Down
8 changes: 8 additions & 0 deletions src/rez/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ def schema(cls):
return Or(*(x.name for x in RezToolsVisibility))


class ExecutableScriptMode_(Str):
@cached_class_property
def schema(cls):
from rez.util import ExecutableScriptMode
return Or(*(x.name for x in ExecutableScriptMode))


class OptionalStrOrFunction(Setting):
schema = Or(None, basestring, callable)

Expand Down Expand Up @@ -303,6 +310,7 @@ def _parse_env_var(self, value):
"documentation_url": Str,
"suite_visibility": SuiteVisibility_,
"rez_tools_visibility": RezToolsVisibility_,
"create_executable_script_mode": ExecutableScriptMode_,
"suite_alias_prefix_char": Char,
"package_definition_python_path": OptionalStr,
"tmpdir": OptionalStr,
Expand Down
25 changes: 17 additions & 8 deletions src/rez/rex.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,17 @@ class ActionInterpreter(object):
"""
expand_env_vars = False

# RegEx that captures environment variables (generic form).
# Extend/Override to regex formats that can captured environment formats
# in other interpreters like shells if needed
ENV_VAR_REGEX = re.compile(
"|".join([
"\\${([^\\{\\}]+?)}", # ${ENVVAR}
"\\$([a-zA-Z_]+[a-zA-Z0-9_]*?)", # $ENVVAR
])
)


def get_output(self, style=OutputStyle.file):
"""Returns any implementation specific data.
Expand Down Expand Up @@ -874,12 +885,6 @@ class NamespaceFormatter(Formatter):
across shells, and avoids some problems with non-curly-braced variables in
some situations.
"""
# Note: the regex used here matches more than just posix environment variable
# names, because special shell expansion characters may be present.
ENV_VAR_REF_1 = "\\${([^\\{\\}]+?)}" # ${ENVVAR}
ENV_VAR_REF_2 = "\\$([a-zA-Z_]+[a-zA-Z0-9_]*?)" # $ENVVAR
ENV_VAR_REF = "%s|%s" % (ENV_VAR_REF_1, ENV_VAR_REF_2)
ENV_VAR_REGEX = re.compile(ENV_VAR_REF)

def __init__(self, namespace):
Formatter.__init__(self)
Expand All @@ -891,7 +896,11 @@ def escape_envvar(matchobj):
value = (x for x in matchobj.groups() if x is not None).next()
return "${{%s}}" % value

format_string_ = re.sub(self.ENV_VAR_REGEX, escape_envvar, format_string)
regex = ActionInterpreter.ENV_VAR_REGEX
if "regex" in kwargs:
regex = kwargs["regex"]

format_string_ = re.sub(regex, escape_envvar, format_string)

# for recursive formatting, where a field has a value we want to expand,
# add kwargs to namespace, so format_field can use them...
Expand Down Expand Up @@ -1251,7 +1260,7 @@ def get_output(self, style=OutputStyle.file):
return self.manager.get_output(style=style)

def expand(self, value):
return self.formatter.format(str(value))
return self.formatter.format(str(value), regex=self.interpreter.ENV_VAR_REGEX)


# Copyright 2013-2016 Allan Johns.
Expand Down
23 changes: 23 additions & 0 deletions src/rez/rezconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,29 @@
suite_alias_prefix_char = "+"


###############################################################################
# Utils
###############################################################################

# Default option on how to create scripts with util.create_executable_script.
# In order to support both windows and other OS it is recommended to set this
# to 'both'.
#
# Possible modes:
# - requested:
# Requested shebang script only. Usually extension-less.
# - py:
# Create .py script that will allow launching scripts on windows,
# if the shell adds .py to PATHEXT. Make sure to use PEP-397 py.exe
# as default application for .py files.
# - platform_specific:
# Will create py script on windows and requested on other platforms
# - both:
# Creates the requested file and a .py script so that scripts can be
# launched without extension from windows and other systems.
create_executable_script_mode = "requested"


###############################################################################
# Appearance
###############################################################################
Expand Down
38 changes: 35 additions & 3 deletions src/rez/shells.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import os
import os.path
import pipes
import re


def get_shell_types():
Expand All @@ -38,7 +39,6 @@ def create_shell(shell=None, **kwargs):
class Shell(ActionInterpreter):
"""Class representing a shell, such as bash or tcsh.
"""

schema_dict = {
"prompt": basestring}

Expand Down Expand Up @@ -160,6 +160,29 @@ def spawn_shell(self, context_file, tmpdir, rcfile=None, norc=False,
"""
raise NotImplementedError

@classmethod
def get_key_token(cls, key, form=0):
"""
Encodes the environment variable into the shell specific form.
Shells might implement multiple forms, but the most common/safest
should be implemented as form 0 or if the form exceeds key_form_count.
Args:
key: Variable name to encode
form: number of token form
Returns:
str of encoded token form
"""
raise NotImplementedError

@classmethod
def key_form_count(cls):
"""
Returns: Number of forms get_key_token supports
"""
raise NotImplementedError

def join(self, command):
"""
Args:
Expand All @@ -171,6 +194,7 @@ def join(self, command):
"""
raise NotImplementedError


class UnixShell(Shell):
"""
A base class for common *nix shells, such as bash and tcsh.
Expand Down Expand Up @@ -396,8 +420,16 @@ def comment(self, value):
def shebang(self):
self._addline("#!%s" % self.executable)

def get_key_token(self, key):
return "${%s}" % key
@classmethod
def get_key_token(cls, key, form=0):
if form == 1:
return "$%s" % key
else:
return "${%s}" % key

@classmethod
def key_form_count(cls):
return 2

def join(self, command):
return shlex_join(command)
Expand Down
Loading

0 comments on commit 9a27edf

Please sign in to comment.