diff --git a/Doc/library/os.rst b/Doc/library/os.rst --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -219,6 +219,17 @@ .. versionadded:: 3.2 +.. function:: get_shell_executable() + + Return a path to the standard system shell executable -- ``sh`` + program on POSIX (default: ``'/bin/sh'``) or ``%ComSpec%`` command + processor on Windows (default: ``%SystemRoot%\system32\cmd.exe``). + + Availability: Unix, Windows + + .. versionadded:: 3.5 + + .. function:: getegid() Return the effective group id of the current process. This corresponds to the diff --git a/Lib/os.py b/Lib/os.py --- a/Lib/os.py +++ b/Lib/os.py @@ -612,6 +612,29 @@ return path_list.split(pathsep) +if sys.platform == "win32": # use sys.platform to accommodate java + def get_shell_executable(): + """Return path to the command processor specified by %ComSpec%. + + Default: %SystemRoot%\system32\cmd.exe + """ + return (environ.get('ComSpec') or + path.join(environ.get('SystemRoot', r'C:\Windows'), + r'system32\cmd.exe')) +else: # POSIX + def get_shell_executable(): + """Return path to the standard system shell executable. + + Default: '/bin/sh' + """ + for dirpath in defpath.split(pathsep): + sh = dirpath + sep + 'sh' #NOTE: interpret '' as '/' + if access(sh, F_OK | X_OK) and path.isfile(sh): + #NOTE: allow symlinks e.g., /bin/sh on Ubuntu may be dash + return sh + return '/bin/sh' #XXX either OS or python are broken if we got here + + # Change environ to automatically call putenv(), unsetenv if they exist. from _collections_abc import MutableMapping diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -734,6 +734,35 @@ self.assertTrue(cm.exception.__suppress_context__) +class ShellExecutableTests(unittest.TestCase): + # test os.get_shell_executable() + def _test_get_shell_executable(self, env): + saved_environ = os.environ + try: + os.environ = env # test with env environment + sh = os.get_shell_executable() + self.assertTrue(sh) # not empty, not None + self.assertTrue(os.path.exists(sh)) + option = "/c" if sys.platform == 'win32' else "-c" + #NOTE: `exit` should work even with an empty environment + self.assertEqual(subprocess.call([sh, option, "exit 5"]), 5) + finally: + os.environ = saved_environ + + def test_get_shell_executable(self): + self._test_get_shell_executable(os.environ) + + def test_get_shell_executable_invalid_nonempty_path(self): + env = {'SystemRoot': os.environ.get('SystemRoot'), + 'PATH': '\0'} + self._test_get_shell_executable(env) + + @unittest.skipIf(sys.platform == 'win32', + "Empty env. is not supported on Win32") + def test_get_shell_executable_empty_environment(self): + self._test_get_shell_executable({}) + + class WalkTests(unittest.TestCase): """Tests for os.walk().""" @@ -2622,6 +2651,7 @@ FileTests, StatAttributeTests, EnvironTests, + ShellExecutableTests, WalkTests, FwalkTests, MakedirTests,