diff --git a/src/rez/bind/hello_world.py b/src/rez/bind/hello_world.py
index cd59d8fbf5..cb100a2652 100644
--- a/src/rez/bind/hello_world.py
+++ b/src/rez/bind/hello_world.py
@@ -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
 
@@ -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!")
@@ -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
@@ -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
diff --git a/src/rez/config.py b/src/rez/config.py
index c91334d26c..f0a2f67626 100644
--- a/src/rez/config.py
+++ b/src/rez/config.py
@@ -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)
 
@@ -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,
diff --git a/src/rez/rex.py b/src/rez/rex.py
index 69766a9416..29fc235ea8 100644
--- a/src/rez/rex.py
+++ b/src/rez/rex.py
@@ -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.
 
@@ -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)
@@ -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...
@@ -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.
diff --git a/src/rez/rezconfig.py b/src/rez/rezconfig.py
index 3e055327d8..44af802b35 100644
--- a/src/rez/rezconfig.py
+++ b/src/rez/rezconfig.py
@@ -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
 ###############################################################################
diff --git a/src/rez/shells.py b/src/rez/shells.py
index 83db28bbab..73104b0184 100644
--- a/src/rez/shells.py
+++ b/src/rez/shells.py
@@ -14,6 +14,7 @@
 import os
 import os.path
 import pipes
+import re
 
 
 def get_shell_types():
@@ -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}
 
@@ -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:
@@ -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.
@@ -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)
diff --git a/src/rez/tests/test_shells.py b/src/rez/tests/test_shells.py
index 419f63a4ad..48a5560224 100644
--- a/src/rez/tests/test_shells.py
+++ b/src/rez/tests/test_shells.py
@@ -6,19 +6,17 @@
 from rez.system import system
 from rez.shells import create_shell
 from rez.resolved_context import ResolvedContext
-from rez.rex import RexExecutor, literal, expandable
+from rez.rex import literal, expandable
 import rez.vendor.unittest2 as unittest
 from rez.tests.util import TestBase, TempdirMixin, shell_dependent, \
     install_dependent
 from rez.util import which
 from rez.bind import hello_world
-from rez.utils.platform_ import platform_
 import subprocess
 import tempfile
 import inspect
 import textwrap
 import os
-import sys
 
 
 def _stdout(proc):
@@ -47,15 +45,10 @@ def tearDownClass(cls):
 
     @classmethod
     def _create_context(cls, pkgs):
-        from rez.config import config
         return ResolvedContext(pkgs, caching=False)
 
-    @shell_dependent(exclude=["cmd"])
+    @shell_dependent()
     def test_no_output(self):
-        # TODO: issues with binding the 'hello_world' package means it is not
-        # possible to run this test on Windows.  The 'hello_world' executable
-        # is not registered correctly on Windows so always returned the
-        # incorrect error code.
         sh = create_shell()
         _, _, _, command = sh.startup_capabilities(command=True)
         if command:
@@ -69,27 +62,20 @@ def test_no_output(self):
                 "startup scripts are printing to stdout. Please remove the "
                 "printout and try again.")
 
-    @shell_dependent(exclude=["cmd"])
+    @shell_dependent()
     def test_command(self):
-        # TODO: issues with binding the 'hello_world' package means it is not
-        # possible to run this test on Windows.  The 'hello_world' executable
-        # is not registered correctly on Windows so always returned the
-        # incorrect error code.
         sh = create_shell()
         _, _, _, command = sh.startup_capabilities(command=True)
 
         if command:
             r = self._create_context(["hello_world"])
-            p = r.execute_shell(command="hello_world",
+            script = "hello_world"
+            p = r.execute_shell(command=script,
                                 stdout=subprocess.PIPE)
             self.assertEqual(_stdout(p), "Hello Rez World!")
 
-    @shell_dependent(exclude=["cmd"])
+    @shell_dependent()
     def test_command_returncode(self):
-        # TODO: issues with binding the 'hello_world' package means it is not
-        # possible to run this test on Windows.  The 'hello_world' executable
-        # is not registered correctly on Windows so always returned the
-        # incorrect error code.
         sh = create_shell()
         _, _, _, command = sh.startup_capabilities(command=True)
 
@@ -145,13 +131,19 @@ def test_rcfile(self):
             self.assertEqual(_stdout(p), "Hello Rez World!")
             os.remove(path)
 
-    @shell_dependent(exclude=["cmd"])
+    @shell_dependent()
     @install_dependent
     def test_rez_env_output(self):
         # here we are making sure that running a command via rez-env prints
         # exactly what we expect.
+        sh = create_shell()
         echo_cmd = which("echo")
-        if not echo_cmd:
+
+        # Certain shells will not find echo.
+        # TODO: If this exception was created for these platforms then if might
+        # be redundant. Can we clarify which platforms this was meant for?
+        has_buildin_echo = sh.name() in ['cmd', 'powershell', 'pwsh']
+        if not echo_cmd and not has_buildin_echo:
             print("\nskipping test, 'echo' command not found.")
             return
 
@@ -189,13 +181,18 @@ def _execute_code(func, expected_output):
 
             out, _ = p.communicate()
             self.assertEqual(p.returncode, 0)
-            token = '\r\n' if platform_.name == 'windows' else '\n'
+
+            # Powershell and Unix uses \n
+            sh = create_shell()
+            token = '\r\n' if sh.name() == 'cmd' else '\n'
+
             output = out.strip().split(token)
             self.assertEqual(output, expected_output)
 
         def _rex_assigning():
-            import os
-            windows = os.name == "nt"
+            from rez.shells import create_shell, UnixShell
+            sh = create_shell()
+            is_powershell = sh.name() in ["powershell", "pwsh"]
 
             def _print(value):
                 env.FOO = value
@@ -203,7 +200,10 @@ def _print(value):
                 # interpreting parts of our output as commands. This can happen
                 # when we include special characters (&, <, >, ^) in a
                 # variable.
-                info('"%FOO%"' if windows else '"${FOO}"')
+                if is_powershell:
+                    info('`"{}`"'.format(sh.get_key_token("FOO")))
+                else:
+                    info('"{}"'.format(sh.get_key_token("FOO")))
 
             env.GREET = "hi"
             env.WHO = "Gary"
@@ -225,12 +225,14 @@ def _print(value):
             _print(literal("hello world"))
             _print(literal("hello 'world'"))
             _print(literal('hello "world"'))
-            _print("hey %WHO%" if windows else "hey $WHO")
-            _print("hey %WHO%" if windows else "hey ${WHO}")
-            _print(expandable("%GREET% " if windows else "${GREET} ").e("%WHO%" if windows else "$WHO"))
-            _print(expandable("%GREET% " if windows else "${GREET} ").l("$WHO"))
+
+            # Generic form of variables
+            _print("hey $WHO")
+            _print("hey ${WHO}")
+            _print(expandable("${GREET} ").e("$WHO"))
+            _print(expandable("${GREET} ").l("$WHO"))
             _print(literal("${WHO}"))
-            _print(literal("${WHO}").e(" %WHO%" if windows else " $WHO"))
+            _print(literal("${WHO}").e(" $WHO"))
 
             # Make sure we are escaping &, <, >, ^ properly.
             _print('hey & world')
@@ -238,6 +240,17 @@ def _print(value):
             _print('hey < world')
             _print('hey ^ world')
 
+            # Platform dependent form of variables.
+            # No need to test in unix shells since their for, matches the
+            # generic form $VAR and ${VAR}.
+            if not isinstance(sh, UnixShell):
+                for i in range(sh.key_form_count()):
+                    _print("hey " + sh.get_key_token("WHO", i))
+                    _print(expandable("${GREET} ").e(sh.get_key_token("WHO", i)))
+                    _print(expandable("${GREET} ").l(sh.get_key_token("WHO", i)))
+                    _print(literal(sh.get_key_token("WHO", i)))
+                    _print(literal(sh.get_key_token("WHO", i)).e(" " + sh.get_key_token("WHO", i)))
+
         expected_output = [
             "ello",
             "ello",
@@ -268,6 +281,21 @@ def _print(value):
             "hey ^ world"
         ]
 
+        # Assertions for other environment variable types
+        from rez.shells import create_shell, UnixShell
+        sh = create_shell()
+        if not isinstance(sh, UnixShell):
+            from rez.shells import create_shell, UnixShell
+            sh = create_shell()
+            for i in range(sh.key_form_count()):
+                expected_output += [
+                    "hey Gary",
+                    "hi Gary",
+                    "hi " + sh.get_key_token("WHO", i),
+                    sh.get_key_token("WHO", i),
+                    sh.get_key_token("WHO", i) + " Gary",
+                ]
+
         # We are wrapping all variable outputs in quotes in order to make sure
         # our shell isn't interpreting our output as instructions when echoing
         # it but this means we need to wrap our expected output as well.
@@ -276,15 +304,15 @@ def _print(value):
         _execute_code(_rex_assigning, expected_output)
 
         def _rex_appending():
-            import os
-            windows = os.name == "nt"
+            from rez.shells import create_shell
+            sh = create_shell()
 
             env.FOO.append("hey")
-            info("%FOO%" if windows else "${FOO}")
+            info(sh.get_key_token("FOO"))
             env.FOO.append(literal("$DAVE"))
-            info("%FOO%" if windows else "${FOO}")
+            info(sh.get_key_token("FOO"))
             env.FOO.append("Dave's not here man")
-            info("%FOO%" if windows else "${FOO}")
+            info(sh.get_key_token("FOO"))
 
         expected_output = [
             "hey",
@@ -294,6 +322,25 @@ def _rex_appending():
 
         _execute_code(_rex_appending, expected_output)
 
+    @shell_dependent()
+    def test_variable_encoding(self):
+        """
+        Sanity test so we can make use of get_key_token in other tests.
+        """
+        sh = create_shell()
+        name = sh.name()
+        if name == "cmd":
+            self.assertEqual("%HELLO%", sh.get_key_token("HELLO", 0))
+            self.assertEqual(1, sh.key_form_count())
+        elif name == "powershell" or name == "pwsh":
+            self.assertEqual("${Env:HELLO}", sh.get_key_token("HELLO", 0))
+            self.assertEqual("$Env:HELLO", sh.get_key_token("HELLO", 1))
+            self.assertEqual(2, sh.key_form_count())
+        else:
+            self.assertEqual("${HELLO}", sh.get_key_token("HELLO", 0))
+            self.assertEqual("$HELLO", sh.get_key_token("HELLO", 1))
+            self.assertEqual(2, sh.key_form_count())
+
     @shell_dependent()
     def test_rex_code_alias(self):
         """Ensure PATH changes do not influence the alias command.
diff --git a/src/rez/util.py b/src/rez/util.py
index b49e84e6c4..260e2c1f69 100644
--- a/src/rez/util.py
+++ b/src/rez/util.py
@@ -10,11 +10,35 @@
 from rez.exceptions import RezError
 from rez.utils.yaml import dump_yaml
 from rez.vendor.progress.bar import Bar
-
+from rez.vendor.enum import Enum
 
 DEV_NULL = open(os.devnull, 'w')
 
 
+class ExecutableScriptMode(Enum):
+    """
+    Which scripts to create with util.create_executable_script.
+    """
+    # Start with 1 to not collide with None checks
+
+    # Requested shebang script only. Usually extension-less.
+    requested = 1
+
+    # Create .py script that will allow launching scripts on
+    # windows without extension, but may require extension on
+    # other systems.
+    py = 2
+
+    # Will create py script on windows and requested on
+    # other platforms
+    platform_specific = 3
+
+    # Creates the requested script and an .py script so that scripts
+    # can be launched without extension from windows and other
+    # systems.
+    both = 4
+
+
 class ProgressBar(Bar):
     def __init__(self, label, max):
         from rez.config import config
@@ -25,19 +49,31 @@ def __init__(self, label, max):
         super(Bar, self).__init__(label, max=max, bar_prefix=' [', bar_suffix='] ')
 
 
-# TODO: use distlib.ScriptMaker
-# TODO: or, do the work ourselves to make this cross platform
-# FIXME: *nix only
-def create_executable_script(filepath, body, program=None):
-    """Create an executable script.
+# TODO: Maybe also allow distlib.ScriptMaker instead of the .py + PATHEXT.
+def create_executable_script(filepath, body, program=None, py_script_mode=None):
+    """
+    Create an executable script. In case a py_script_mode has been set to create
+    a .py script the shell is expected to have the PATHEXT environment
+    variable to include ".PY" in order to properly launch the command without
+    the .py extension.
 
     Args:
         filepath (str): File to create.
         body (str or callable): Contents of the script. If a callable, its code
             is used as the script body.
-        program (str): Name of program to launch the script, 'python' if None
+        program (str): Name of program to launch the script. Default is 'python'
+        py_script_mode(ExecutableScriptMode): What kind of script to create.
+            Defaults to rezconfig.create_executable_script_mode.
+    Returns:
+        List of filepaths of created scripts. This may differ from the supplied
+        filepath depending on the py_script_mode
+
     """
+    from rez.config import config
+    from rez.utils.platform_ import platform_
     program = program or "python"
+    py_script_mode = py_script_mode or config.create_executable_script_mode
+
     if callable(body):
         from rez.utils.sourcecode import SourceCode
         code = SourceCode(func=body)
@@ -46,18 +82,70 @@ def create_executable_script(filepath, body, program=None):
     if not body.endswith('\n'):
         body += '\n'
 
-    with open(filepath, 'w') as f:
-        # TODO: make cross platform
-        f.write("#!/usr/bin/env %s\n" % program)
-        f.write(body)
-
-    # TODO: Although Windows supports os.chmod you can only set the readonly
-    # flag. Setting the file readonly breaks the unit tests that expect to
-    # clean up the files once the test has run.  Temporarily we don't bother
-    # setting the permissions, but this will need to change.
-    if os.name == "posix":
-    	os.chmod(filepath, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
-                 | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+    # Windows does not support shebang, but it will run with
+    # default python, or in case of later python versions 'py' that should
+    # try to use sensible python interpreters depending on the shebang line.
+    # Compare PEP-397.
+    # In order to execution to work from windows we need to create a .py
+    # file and set the PATHEXT to include .py (as done by the shell plugins)
+    # So depending on the py_script_mode we might need to create more then
+    # one script
+
+    script_filepaths = [filepath]
+    if program == "python":
+        script_filepaths = _get_python_script_files(filepath, py_script_mode,
+                                                    platform_.name)
+
+    for current_filepath in script_filepaths:
+        with open(current_filepath, 'w') as f:
+            # TODO: make cross platform
+            f.write("#!/usr/bin/env %s\n" % program)
+            f.write(body)
+
+        # TODO: Although Windows supports os.chmod you can only set the readonly
+        # flag. Setting the file readonly breaks the unit tests that expect to
+        # clean up the files once the test has run.  Temporarily we don't bother
+        # setting the permissions, but this will need to change.
+        if os.name == "posix":
+            os.chmod(current_filepath, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
+                     | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+
+    return script_filepaths
+
+
+def _get_python_script_files(filepath, py_script_mode, platform):
+    """
+    Evaluates the py_script_mode for the requested filepath on the given
+    platform.
+
+    Args:
+        filepath: requested filepath
+        py_script_mode (ExecutableScriptMode):
+        platform (str): Platform to evaluate the script files for
+
+    Returns:
+
+    """
+    script_filepaths = []
+    base_filepath, extension = os.path.splitext(filepath)
+    has_py_ext = extension == ".py"
+    is_windows = platform == "windows"
+
+    if py_script_mode == ExecutableScriptMode.requested or \
+            py_script_mode == ExecutableScriptMode.both or \
+            (py_script_mode == ExecutableScriptMode.py and has_py_ext) or \
+            (py_script_mode == ExecutableScriptMode.platform_specific and
+             is_windows and has_py_ext):
+        script_filepaths.append(filepath)
+
+    if not has_py_ext and \
+            ((py_script_mode == ExecutableScriptMode.both) or
+             (py_script_mode == ExecutableScriptMode.py) or
+             (py_script_mode == ExecutableScriptMode.platform_specific and
+              is_windows)):
+        script_filepaths.append(base_filepath + ".py")
+
+    return script_filepaths
 
 
 def create_forwarding_script(filepath, module, func_name, *nargs, **kwargs):
diff --git a/src/rezplugins/shell/cmd.py b/src/rezplugins/shell/cmd.py
index 4a9d07d436..7d77aec2d9 100644
--- a/src/rezplugins/shell/cmd.py
+++ b/src/rezplugins/shell/cmd.py
@@ -21,6 +21,9 @@ class CMD(Shell):
     syspaths = None
     _executable = 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
@@ -182,6 +185,12 @@ def _create_ex():
             executor.interpreter._saferefenv('REZ_ENV_PROMPT')
             executor.env.REZ_ENV_PROMPT = literal(newprompt)
 
+        # Make .py launch within cmd without extension.
+        if self.settings.additional_pathext:
+            executor.command('set PATHEXT=%PATHEXT%;{}'.format(
+                ";".join(self.settings.additional_pathext)
+            ))
+
         if startup_sequence["command"] is not None:
             _record_shell(executor, files=startup_sequence["files"])
             shell_command = startup_sequence["command"]
@@ -247,9 +256,19 @@ def escape_string(self, value):
             str: The value escaped for Windows.
 
         """
-        if isinstance(value, EscapedString):
-            return value.formatted(self._escaper)
-        return self._escaper(value)
+        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:
+                txt = self._escaper(txt)
+            result += txt
+        return result
 
     def _saferefenv(self, key):
         pass
@@ -297,8 +316,13 @@ def source(self, value):
     def command(self, value):
         self._addline(value)
 
-    def get_key_token(self, key):
-        return "%%%s%%" % key
+    @classmethod
+    def get_key_token(cls, key, form=0):
+        return "%{}%".format(key)
+
+    @classmethod
+    def key_form_count(cls):
+        return 1
 
     def join(self, command):
         return " ".join(command)
diff --git a/src/rezplugins/shell/powershell.py b/src/rezplugins/shell/powershell.py
index 3d7f92f823..725c109c80 100644
--- a/src/rezplugins/shell/powershell.py
+++ b/src/rezplugins/shell/powershell.py
@@ -1,7 +1,7 @@
 """Windows PowerShell 5"""
 
 from rez.config import config
-from rez.rex import RexExecutor, OutputStyle, EscapedString
+from rez.rex import RexExecutor, OutputStyle, EscapedString, literal
 from rez.shells import Shell
 from rez.utils.system import popen
 from rez.utils.platform_ import platform_
@@ -12,28 +12,43 @@
 import re
 
 
-class PowerShell(Shell):
+class PowerShellBase(Shell):
+    """
+    Abstract base class for Powershell-like shells.
+    """
+    expand_env_vars = True
+
     syspaths = None
     _executable = None
 
-    # 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))
+    # Make sure that the $Env:VAR formats come before the $VAR formats since
+    # Powershell Environment variables are ambiguous with Unix paths.
+    ENV_VAR_REGEX = re.compile(
+        "|".join([
+            "\\$[Ee][Nn][Vv]:([a-zA-Z_]+[a-zA-Z0-9_]*?)",  # $Env:ENVVAR
+            Shell.ENV_VAR_REGEX.pattern,                   # Generic form
+        ])
+    )
+
+    @staticmethod
+    def _escape_quotes(s):
+        return s.replace('"', '`"').replace("'", "`'")
+
+    @staticmethod
+    def _escape_vars(s):
+        return s.replace('$', '`$')
 
     @property
     def executable(cls):
-        if cls._executable is None:
-            cls._executable = Shell.find_executable('powershell')
-        return cls._executable
+        raise NotImplementedError
 
     @classmethod
     def name(cls):
-        return 'powershell'
+        raise NotImplementedError
 
     @classmethod
     def file_extension(cls):
-        return 'ps1'
+        raise NotImplementedError
 
     @classmethod
     def startup_capabilities(cls,
@@ -177,6 +192,14 @@ def _record_shell(ex, files, bind_rez=True, print_msg=False):
                           files=startup_sequence["files"],
                           print_msg=(not quiet))
 
+        # Make .py launch within cmd without extension.
+        # For PowerShell this will also execute in the same window, so that
+        # stdout can be captured.
+        if self.settings.additional_pathext:
+            executor.command('$Env:PATHEXT = $Env:PATHEXT + ";{}"'.format(
+                ";".join(self.settings.additional_pathext)
+            ))
+
         if shell_command:
             executor.command(shell_command)
 
@@ -198,7 +221,9 @@ def _record_shell(ex, files, bind_rez=True, print_msg=False):
                 cmd = pre_command.rstrip().split()
 
         cmd += [self.executable]
-        cmd += ['. "{}"'.format(target_file)]
+
+        # Generic form of sourcing that works in powershell and pwsh
+        cmd += ['-File', '{}'.format(target_file)]
 
         if shell_command is None:
             cmd.insert(1, "-noexit")
@@ -222,18 +247,17 @@ def get_output(self, style=OutputStyle.file):
         return script
 
     def escape_string(self, value):
-        """Escape the <, >, ^, and & special characters reserved by Windows.
-
-        Args:
-            value (str/EscapedString): String or already escaped string.
-
-        Returns:
-            str: The value escaped for Windows.
-
-        """
-        if isinstance(value, EscapedString):
-            return value.formatted(self._escaper)
-        return self._escaper(value)
+        value = EscapedString.promote(value)
+        value = value.expanduser()
+        result = ''
+
+        for is_literal, txt in value.strings:
+            if is_literal:
+                txt = self._escape_quotes(self._escape_vars(txt))
+            else:
+                txt = self._escape_quotes(txt)
+            result += txt
+        return result
 
     def _saferefenv(self, key):
         pass
@@ -243,7 +267,11 @@ def shebang(self):
 
     def setenv(self, key, value):
         value = self.escape_string(value)
-        self._addline('$env:{0} = "{1}"'.format(key, value))
+        self._addline('$Env:{0} = "{1}"'.format(key, value))
+
+    def appendenv(self, key, value):
+        value = self.escape_string(value)
+        self._addline('$Env:{0} = "$Env:{0};{1}"'.format(key, value))
 
     def unsetenv(self, key):
         self._addline(r"Remove-Item Env:\%s" % key)
@@ -276,13 +304,38 @@ def source(self, value):
     def command(self, value):
         self._addline(value)
 
-    def get_key_token(self, key):
-        return "$env:%s" % key
+    @classmethod
+    def get_key_token(cls, key, form=0):
+        if form == 1:
+            return "$Env:%s" % key
+        else:
+            return "${Env:%s}" % key
+
+    @classmethod
+    def key_form_count(cls):
+        return 2
 
     def join(self, command):
         return " ".join(command)
 
 
+class PowerShell(PowerShellBase):
+
+    @property
+    def executable(cls):
+        if cls._executable is None:
+            cls._executable = Shell.find_executable('powershell')
+        return cls._executable
+
+    @classmethod
+    def name(cls):
+        return 'powershell'
+
+    @classmethod
+    def file_extension(cls):
+        return 'ps1'
+
+
 def register_plugin():
     if platform_.name == "windows":
         return PowerShell
diff --git a/src/rezplugins/shell/pwsh.py b/src/rezplugins/shell/pwsh.py
index 3641d8ce1b..542b7aa627 100644
--- a/src/rezplugins/shell/pwsh.py
+++ b/src/rezplugins/shell/pwsh.py
@@ -6,20 +6,14 @@
 from rez.utils.system import popen
 from rez.utils.platform_ import platform_
 from rez.backport.shutilwhich import which
+from rezplugins.shell.powershell import PowerShellBase
 from functools import partial
 from subprocess import PIPE
 import os
 import re
 
 
-class PowerShell(Shell):
-    syspaths = None
-    _executable = None
-
-    # 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))
+class PowerShellCore(PowerShellBase):
 
     @property
     def executable(cls):
@@ -35,253 +29,10 @@ def name(cls):
     def file_extension(cls):
         return 'ps1'
 
-    @classmethod
-    def startup_capabilities(cls,
-                             rcfile=False,
-                             norc=False,
-                             stdin=False,
-                             command=False):
-        cls._unsupported_option('rcfile', rcfile)
-        cls._unsupported_option('norc', norc)
-        cls._unsupported_option('stdin', stdin)
-        rcfile = False
-        norc = False
-        stdin = False
-        return (rcfile, norc, stdin, command)
-
-    @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))
-
-    @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
-
-        # detect system paths using registry
-        def gen_expected_regex(parts):
-            whitespace = r"[\s]+"
-            return whitespace.join(parts)
-
-        paths = []
-
-        cmd = [
-            "REG", "QUERY",
-            ("HKLM\\SYSTEM\\CurrentControlSet\\"
-             "Control\\Session Manager\\Environment"), "/v", "PATH"
-        ]
-
-        expected = gen_expected_regex([
-            ("HKEY_LOCAL_MACHINE\\\\SYSTEM\\\\CurrentControlSet\\\\"
-             "Control\\\\Session Manager\\\\Environment"), "PATH",
-            "REG_(EXPAND_)?SZ", "(.*)"
-        ])
-
-        p = popen(cmd,
-                  stdout=PIPE,
-                  stderr=PIPE,
-                  universal_newlines=True,
-                  shell=True)
-        out_, _ = p.communicate()
-        out_ = out_.strip()
-
-        if p.returncode == 0:
-            match = re.match(expected, out_)
-            if match:
-                paths.extend(match.group(2).split(os.pathsep))
-
-        cmd = ["REG", "QUERY", "HKCU\\Environment", "/v", "PATH"]
-
-        expected = gen_expected_regex([
-            "HKEY_CURRENT_USER\\\\Environment", "PATH", "REG_(EXPAND_)?SZ",
-            "(.*)"
-        ])
-
-        p = popen(cmd,
-                  stdout=PIPE,
-                  stderr=PIPE,
-                  universal_newlines=True,
-                  shell=True)
-        out_, _ = p.communicate()
-        out_ = out_.strip()
-
-        if p.returncode == 0:
-            match = re.match(expected, out_)
-            if match:
-                paths.extend(match.group(2).split(os.pathsep))
-
-        cls.syspaths = list(set([x for x in paths if x]))
-
-        # add Rez binaries
-        exe = which("rez-env")
-        assert exe, "Could not find rez binary, this is a bug"
-        rez_bin_dir = os.path.dirname(exe)
-        cls.syspaths.insert(0, rez_bin_dir)
-
-        return cls.syspaths
-
-    def _bind_interactive_rez(self):
-        if config.set_prompt and self.settings.prompt:
-            self._addline('Function prompt {"%s"}' % self.settings.prompt)
-
-    def spawn_shell(self,
-                    context_file,
-                    tmpdir,
-                    rcfile=None,
-                    norc=False,
-                    stdin=False,
-                    command=None,
-                    env=None,
-                    quiet=False,
-                    pre_command=None,
-                    **Popen_args):
-
-        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 bind_rez:
-                ex.interpreter._bind_interactive_rez()
-            if print_msg and not quiet:
-                # Rez may not be available
-                ex.command("Try { rez context } Catch { }")
-
-        executor = RexExecutor(interpreter=self.new_shell(),
-                               parent_environ={},
-                               add_default_namespaces=False)
-
-        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:
-            executor.command(shell_command)
-
-        # Forward exit call to parent PowerShell process
-        executor.command("exit $LastExitCode")
-
-        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)
-
-        cmd = []
-        if pre_command:
-            cmd = pre_command
-
-            if not isinstance(cmd, (tuple, list)):
-                cmd = pre_command.rstrip().split()
-
-        cmd += [self.executable]
-        cmd += ['{}'.format(target_file)]
-
-        if shell_command is None:
-            cmd.insert(1, "-noexit")
-
-        p = popen(cmd, env=env, universal_newlines=True, **Popen_args)
-        return p
-
-    def get_output(self, style=OutputStyle.file):
-        if style == OutputStyle.file:
-            script = '\n'.join(self._lines) + '\n'
-        else:
-            lines = []
-            for line in self._lines:
-                if line.startswith('#'):
-                    continue
-
-                line = line.rstrip()
-                lines.append(line)
-
-            script = '&& '.join(lines)
-        return script
-
-    def escape_string(self, value):
-        """Escape the <, >, ^, and & special characters reserved by Windows.
-
-        Args:
-            value (str/EscapedString): String or already escaped string.
-
-        Returns:
-            str: The value escaped for Windows.
-
-        """
-        if isinstance(value, EscapedString):
-            return value.formatted(self._escaper)
-        return self._escaper(value)
-
-    def _saferefenv(self, key):
-        pass
-
-    def shebang(self):
-        pass
-
-    def setenv(self, key, value):
-        value = self.escape_string(value)
-        self._addline('$env:{0} = "{1}"'.format(key, value))
-
-    def unsetenv(self, key):
-        self._addline(r"Remove-Item Env:\%s" % key)
-
-    def resetenv(self, key, value, friends=None):
-        self._addline(self.setenv(key, value))
-
-    def alias(self, key, value):
-        value = EscapedString.disallow(value)
-        cmd = "function {key}() {{ {value} $args }}"
-        self._addline(cmd.format(key=key, value=value))
-
-    def comment(self, value):
-        for line in value.split('\n'):
-            self._addline('# %s' % line)
-
-    def info(self, value):
-        for line in value.split('\n'):
-            self._addline('Write-Host %s' % line)
-
-    def error(self, value):
-        for line in value.split('\n'):
-            self._addline('Write-Error "%s"' % line)
-
-    def source(self, value):
-        self._addline(". \"%s\"" % value)
-
-    def command(self, value):
-        self._addline(value)
-
-    def get_key_token(self, key):
-        return "$env:%s" % key
-
-    def join(self, command):
-        return " ".join(command)
-
 
 def register_plugin():
-    if platform_.name == "windows":
-        return PowerShell
+    # Platform independent
+    return PowerShellCore
 
 
 # Copyright 2013-2016 Allan Johns.
diff --git a/src/rezplugins/shell/rezconfig b/src/rezplugins/shell/rezconfig
index 8e40dc6107..271050eef9 100644
--- a/src/rezplugins/shell/rezconfig
+++ b/src/rezplugins/shell/rezconfig
@@ -15,9 +15,12 @@ zsh:
 
 cmd:
     prompt: '$G'
+    additional_pathext: ['.PY']
 
 powershell:
     prompt: '> $ '
+    additional_pathext: ['.PY']
 
 pwsh:
     prompt: '> $ '
+    additional_pathext: ['.PY']