[Jython-checkins] jython: Many compatibility fixes for launcher (bin/jython, bin/jython.exe)

jim.baker jython-checkins at python.org
Mon Apr 20 21:22:57 CEST 2015


https://hg.python.org/jython/rev/585cc365d4bd
changeset:   7687:585cc365d4bd
user:        Jim Baker <jim.baker at rackspace.com>
date:        Mon Apr 20 13:22:51 2015 -0600
summary:
  Many compatibility fixes for launcher (bin/jython, bin/jython.exe)

Now supports CLASSPATH, and correctly parses JAVA_OPTS and JYTHON_OPTS
for inclusion into the command line for org.python.util.jython, which
is the actual main program for command line Jython.

Updated installer so it generates a shebang line for the launcher that
is specific to the CPython 2.7 installation that's available. This
change allows for -E to be passed as an option, which does not work if
shebang uses #!/usr/bin/env to select, as it previously did.

More robust testing, including unit tests that was ported from the now
removed test_bat_jy, but now applied to all platforms.

RC2 introduced a bug where jython -classpath CLASSPATH could work;
this has been removed in favor of jython -J-classpath CLASSPATH (old
behavior); this interferes with the processing of the -c option.

Fixes http://bugs.jython.org/issue2311 and http://bugs.jython.org/issue2319

files:
  Lib/test/test_bat_jy.py                                              |  411 ----------
  Lib/test/test_jython_launcher.py                                     |  260 ++++++
  installer/src/java/org/python/util/install/StartScriptGenerator.java |   55 +-
  src/shell/jython.exe                                                 |  Bin 
  src/shell/jython.py                                                  |  322 +++++--
  5 files changed, 510 insertions(+), 538 deletions(-)


diff --git a/Lib/test/test_bat_jy.py b/Lib/test/test_bat_jy.py
deleted file mode 100644
--- a/Lib/test/test_bat_jy.py
+++ /dev/null
@@ -1,411 +0,0 @@
-'''Tests jython.bat using the --print option'''
-
-import os
-import sys
-import unittest
-import tempfile
-
-from test import test_support
-
-from java.lang import IllegalThreadStateException
-from java.lang import Runtime
-from java.lang import System
-from java.lang import Thread
-from java.io import File
-from java.io import BufferedReader;
-from java.io import InputStreamReader;
-
-class Monitor(Thread):
-    def __init__(self, process):
-        self.process = process
-        self.output = ''
-
-    def run(self):
-        reader = BufferedReader(InputStreamReader(self.getStream()))
-        try:
-            line = reader.readLine()
-            while line:
-                self.output += line
-                line = reader.readLine()
-        finally:
-            reader.close()
-    
-    def getOutput(self):
-        return self.output
-
-class StdoutMonitor(Monitor):
-    def __init_(self, process):
-        Monitor.__init__(self, process)
-
-    def getStream(self):
-        return self.process.getInputStream()
-
-class StderrMonitor(Monitor):
-    def __init_(self, process):
-        Monitor.__init__(self, process)
-
-    def getStream(self):
-        return self.process.getErrorStream()
-
-class StarterProcess:
-    def writeStarter(self, args, javaHome, jythonHome, jythonOpts, internals=False):
-        (starter, starterPath) = tempfile.mkstemp(suffix='.bat', prefix='starter', text=True)
-        starter.close()
-        outfilePath = starterPath[:-4] + '.out'
-        starter = open(starterPath, 'w') # open starter as simple file
-        try:
-            if javaHome:
-                starter.write('set JAVA_HOME=%s\n' % javaHome)
-            if jythonHome:
-                starter.write('set JYTHON_HOME=%s\n' % jythonHome)
-            if jythonOpts:
-                starter.write('set JYTHON_OPTS=%s\n' % jythonOpts)
-            if internals:
-                starter.write('set _JYTHON_OPTS=leaking_internals\n')
-                starter.write('set _JYTHON_HOME=c:/leaking/internals\n')
-            starter.write(self.buildCommand(args, outfilePath))
-            return (starterPath, outfilePath)
-        finally:
-            starter.close()
-
-    def buildCommand(self, args, outfilePath):
-        line = ''
-        for arg in args:
-            line += arg
-            line += ' '
-        line += '> '
-        line += outfilePath
-        line += ' 2>&1'
-        return line
-
-    def getOutput(self, outfilePath):
-        lines = ''
-        outfile = open(outfilePath, 'r')
-        try:
-            for line in outfile.readlines():
-                lines += line
-        finally:
-            outfile.close()
-        return lines
-
-    def isAlive(self, process):
-        try:
-            process.exitValue()
-            return False
-        except IllegalThreadStateException:
-            return True
-
-    def run(self, args, javaHome, jythonHome, jythonOpts, internals=False):
-        ''' creates a start script, executes it and captures the output '''
-        (starterPath, outfilePath) = self.writeStarter(args, javaHome, jythonHome, jythonOpts, internals)
-        try:
-            process = Runtime.getRuntime().exec(starterPath)
-            stdoutMonitor = StdoutMonitor(process)
-            stderrMonitor = StderrMonitor(process)
-            stdoutMonitor.start()
-            stderrMonitor.start()
-            while self.isAlive(process):
-                Thread.sleep(300)
-            return self.getOutput(outfilePath)
-        finally:
-            os.remove(starterPath)
-            os.remove(outfilePath)
-
-class BaseTest(unittest.TestCase):
-    def quote(self, s):
-        return '"' + s + '"'
-
-    def unquote(self, s):
-        if len(s) > 0:
-            if s[:1] == '"':
-                s = s[1:]
-        if len(s) > 0:
-            if s[-1:] == '"':
-                s = s[:-1]
-        return s
-
-    def getHomeDir(self):
-        ex = sys.executable
-        tail = ex[-15:]
-        if tail == '\\bin\\jython.bat':
-            home = ex[:-15]
-        else:
-            home = ex[:-11] # \jython.bat
-        return home
-
-    def assertOutput(self, flags=None, javaHome=None, jythonHome=None, jythonOpts=None, internals=False):
-        args = [self.quote(sys.executable), '--print']
-        memory = None
-        stack = None
-        prop = None
-        jythonArgs = None
-        boot = False
-        jdb = False
-        if flags:
-            for flag in flags:
-                if flag[:2] == '-J':
-                    if flag[2:6] == '-Xmx':
-                        memory = flag[6:]
-                    elif flag[2:6] == '-Xss':
-                        stack = flag[6:]
-                    elif flag[2:4] == '-D':
-                        prop = flag[2:]
-                elif flag[:2] == '--':
-                    if flag[2:6] == 'boot':
-                        boot = True
-                    elif flag[2:5] == 'jdb':
-                        jdb = True
-                else:
-                    if jythonArgs:
-                        jythonArgs += ' '
-                        jythonArgs += flag
-                    else:
-                        jythonArgs = flag
-                    jythonArgs = jythonArgs.replace('%%', '%') # workaround two .bat files
-                args.append(flag)
-        process = StarterProcess()
-        out = process.run(args, javaHome, jythonHome, jythonOpts, internals)
-        self.assertNotEquals('', out)
-        homeIdx = out.find('-Dpython.home=')
-        java = 'java'
-        if javaHome:
-            java = self.quote(self.unquote(javaHome) + '\\bin\\java')
-        elif jdb:
-            java = 'jdb'
-        if not memory:
-            memory = '512m'
-        if not stack:
-            stack = '1152k'
-        beginning = java + ' '
-        if prop:
-            beginning += ' ' + prop
-        beginning += ' -Xmx' + memory + ' -Xss' + stack + ' '
-        self.assertEquals(beginning, out[:homeIdx])
-        executableIdx = out.find('-Dpython.executable=')
-        homeDir = self.getHomeDir()
-        if jythonHome:
-            homeDir = self.unquote(jythonHome)
-        home = '-Dpython.home=' + self.quote(homeDir) + ' '
-        self.assertEquals(home, out[homeIdx:executableIdx])
-        if boot:
-            classpathFlag = '-Xbootclasspath/a:'
-        else:
-            classpathFlag = '-classpath'
-        classpathIdx = out.find(classpathFlag)
-        executable = '-Dpython.executable=' + self.quote(sys.executable) + ' '
-        if not boot:
-            executable += ' '
-        self.assertEquals(executable, out[executableIdx:classpathIdx])
-        # ignore full contents of classpath at the moment
-        classIdx = out.find('org.python.util.jython')
-        self.assertTrue(classIdx > classpathIdx)
-        restIdx = classIdx + len('org.python.util.jython')
-        rest = out[restIdx:].strip()
-        if jythonOpts:
-            self.assertEquals(self.quote(jythonOpts), rest)
-        else:
-            if jythonArgs:
-                self.assertEquals(jythonArgs, rest)
-            else:
-                self.assertEquals('', rest)
-
-class VanillaTest(BaseTest):
-    def test_plain(self):
-        self.assertOutput()
-
-class JavaHomeTest(BaseTest):
-    def test_unquoted(self):
-        # for the build bot, try to specify a real java home
-        javaHome = System.getProperty('java.home', 'C:\\Program Files\\Java\\someJava')
-        self.assertOutput(javaHome=javaHome)
-
-    def test_quoted(self):
-        self.assertOutput(javaHome=self.quote('C:\\Program Files\\Java\\someJava'))
-
-    # this currently fails, meaning we accept only quoted (x86) homes ...
-    def __test_x86_unquoted(self):
-        self.assertOutput(javaHome='C:\\Program Files (x86)\\Java\\someJava')
-
-    def test_x86_quoted(self):
-        self.assertOutput(javaHome=self.quote('C:\\Program Files (x86)\\Java\\someJava'))
-        
-class JythonHomeTest(BaseTest):
-    def createJythonJar(self, parentDir):
-        jar = File(parentDir, 'jython.jar')
-        if not jar.exists():
-            self.assertTrue(jar.createNewFile())
-        return jar
-
-    def cleanup(self, tmpdir, jar=None):
-        if jar and jar.exists():
-            self.assertTrue(jar.delete())
-        os.rmdir(tmpdir)
-
-    def test_unquoted(self):
-        jythonHomeDir = tempfile.mkdtemp()
-        jar = self.createJythonJar(jythonHomeDir)
-        self.assertOutput(jythonHome=jythonHomeDir)
-        self.cleanup(jythonHomeDir, jar)
-
-    def test_quoted(self):
-        jythonHomeDir = tempfile.mkdtemp()
-        jar = self.createJythonJar(jythonHomeDir)
-        self.assertOutput(jythonHome=self.quote(jythonHomeDir))
-        self.cleanup(jythonHomeDir, jar)
-
-class JythonOptsTest(BaseTest):
-    def test_single(self):
-        self.assertOutput(jythonOpts='myOpt')
-        
-    def test_multiple(self):
-        self.assertOutput(jythonOpts='some arbitrary options')
-
-class InternalsTest(BaseTest):
-    def test_no_leaks(self):
-        self.assertOutput(internals=True)
-
-class JavaOptsTest(BaseTest):
-    def test_memory(self):
-        self.assertOutput(['-J-Xmx321m'])
-
-    def test_stack(self):
-        self.assertOutput(['-J-Xss321k'])
-
-    def test_property(self):
-        self.assertOutput(['-J-DmyProperty=myValue'])
-
-    def test_property_singlequote(self):
-        self.assertOutput(["-J-DmyProperty='myValue'"]) 
-
-    # a space inside value does not work in jython.bat
-    def __test_property_singlequote_space(self):
-        self.assertOutput(["-J-DmyProperty='my Value'"])
-
-    def test_property_doublequote(self):
-        self.assertOutput(['-J-DmyProperty="myValue"']) 
-
-    # a space inside value does not work in jython.bat
-    def __test_property_doublequote_space(self):
-        self.assertOutput(['-J-DmyProperty="my Value"'])
-
-    def test_property_underscore(self):
-        self.assertOutput(['-J-Dmy_Property=my_Value'])
-
-class ArgsTest(BaseTest):
-    def test_file(self):
-        self.assertOutput(['test.py'])
-    
-    def test_dash(self):
-        self.assertOutput(['-i'])
-
-    def test_combined(self):
-        self.assertOutput(['-W', 'action', 'line'])
-
-    def test_singlequoted(self):
-        self.assertOutput(['-c', "'import sys;'"])
-
-    def test_doublequoted(self):
-        self.assertOutput(['-c', '"print \'something\'"'])
-
-    def test_nestedquotes(self):
-        self.assertOutput(['-c', '"print \'something \"really\" cool\'"'])
-
-    def test_nestedquotes2(self):
-        self.assertOutput(['-c', "'print \"something \'really\' cool\"'"])
-
-    def test_underscored(self):
-        self.assertOutput(['-jar', 'my_stuff.jar'])
-    
-    def test_property(self):
-        self.assertOutput(['-DmyProperty=myValue'])
-
-    def test_property_underscored(self):
-        self.assertOutput(['-DmyProperty=my_Value'])
-
-    def test_property_singlequoted(self):
-        self.assertOutput(["-DmyProperty='my_Value'"])
-
-    def test_property_doublequoted(self):
-        self.assertOutput(['-DmyProperty="my_Value"'])
-
-class DoubleDashTest(BaseTest):
-    def test_boot(self):
-        self.assertOutput(['--boot'])
-
-    def test_jdb(self):
-        self.assertOutput(['--jdb'])
-
-class GlobPatternTest(BaseTest):
-    def test_star_nonexisting(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '*.nonexisting', '*.nonexisting'])
-
-    def test_star_nonexisting_doublequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"*.nonexisting"', '"*.nonexisting"'])
-
-    def test_star_nonexistingfile_singlequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'*.nonexisting'", "'*.nonexisting'"])
-
-    def test_star_existing(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '*.bat', '*.bat'])
-
-    def test_star_existing_doublequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"*.bat"', '"*.bat"'])
-
-    def test_star_existing_singlequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'*.bat'", "'*.bat'"])
-
-class ArgsSpacesTest(BaseTest):
-    def test_doublequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', '"part1 part2"', '2nd'])
-
-    def test_singlequoted(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "'part1 part2'", '2nd'])
-
-    # this test currently fails
-    def __test_unbalanced_doublequote(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'Scarlet O"Hara', '2nd'])
-
-    def test_unbalanced_singlequote(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', "Scarlet O'Hara", '2nd'])
-
-class ArgsSpecialCharsTest(BaseTest):
-    # exclamation marks are still very special ...
-    def __test_exclamationmark(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo!', 'ba!r', '!baz', '!'])
-
-    # because we go through a starter.bat file, we have to simulate % with %%
-    def test_percentsign(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo%%1', '%%1bar', '%%1', '%%'])
-
-    def test_colon(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo:', ':bar'])
-
-    # a semicolon at the beginning of an arg currently fails (e.g. ;bar)
-    def test_semicolon(self):
-        self.assertOutput(['-c', 'import sys; print sys.argv[1:]', 'foo;'])
-
-class DummyTest(unittest.TestCase):
-    def test_nothing(self):
-        pass
-
-def test_main():
-    if os._name == 'nt':
-        test_support.run_unittest(VanillaTest,
-                                  JavaHomeTest,
-                                  JythonHomeTest,
-                                  JythonOptsTest,
-                                  InternalsTest,
-                                  JavaOptsTest,
-                                  ArgsTest,
-                                  DoubleDashTest,
-                                  GlobPatternTest,
-                                  ArgsSpacesTest,
-                                  ArgsSpecialCharsTest)
-    else:
-        # provide at least one test for the other platforms - happier build bots
-        test_support.run_unittest(DummyTest)
-
-
-if __name__ == '__main__':
-    test_main()
-        
diff --git a/Lib/test/test_jython_launcher.py b/Lib/test/test_jython_launcher.py
new file mode 100644
--- /dev/null
+++ b/Lib/test/test_jython_launcher.py
@@ -0,0 +1,260 @@
+# Cross-platform testing of the Jython launcher (bin/jython or bin/jython.exe) using --print
+# Replaces test_bat_jy, with some test cases directly used with minor adaptation from that test
+
+import os
+import pprint
+import shlex
+import subprocess
+import sys
+import unittest
+from collections import OrderedDict
+from test import test_support
+
+launcher = None
+uname = None
+is_windows = False
+some_jar = os.path.join(os.sep, "a", "b", "c", "some.jar")
+
+
+def get_launcher(executable):
+    # accounts for continued presence of jython bash script
+    # when not installed with the installer or if CPython 2.7
+    # is not available
+    if os._name == "nt":
+        return executable
+    exec_path = os.path.dirname(sys.executable)
+    jython_py = os.path.join(exec_path, "jython.py")
+    if os.path.exists(jython_py):
+        return jython_py
+    else:
+        # presumably jython.py has been renamed to jython, generally
+        # by the installer
+        return executable
+
+
+def get_uname():
+    _uname = None
+    try:
+        _uname = subprocess.check_output(["uname"]).strip().lower()
+        if _uname.startswith("cygwin"):
+            _uname = "cygwin"
+    except OSError:
+        if os._name == "nt":
+            _uname = "windows"
+    return _uname
+
+
+def classpath_delimiter():
+    return ";" if (os._name == "nt" or uname == "cygwin") else ":"
+
+
+class TestLauncher(unittest.TestCase):
+    
+    def get_cmdline(self, cmd, env):
+
+        output = subprocess.check_output(cmd, env=env).rstrip()
+        if is_windows:
+            return subprocess._cmdline2list(output)
+        else:
+            return shlex.split(output)
+
+    def get_newenv(self, env=os.environ):
+        newenv = env.copy()
+        for var in ("CLASSPATH",
+                    "JAVA_MEM", "JAVA_HOME", "JAVA_OPTS", "JAVA_STACK",
+                    "JYTHON_HOME", "JYTHON_OPTS"):
+            try:
+                del newenv[var]
+            except KeyError:
+                pass
+        return newenv
+
+    def get_properties(self, args):
+        props = OrderedDict()
+        for arg in args:
+            if arg.startswith("-D"):
+                k, v = arg[2:].split("=")
+                props[k] = v
+        return props
+            
+    def test_classpath_env(self):
+        env = self.get_newenv()
+        env["CLASSPATH"] = some_jar
+        args = self.get_cmdline([launcher, "--print"], env=env)
+        it = iter(args)
+        while it:
+            arg = next(it)
+            if arg == "-classpath":
+                self.assertEqual(next(it).split(classpath_delimiter())[-1], some_jar)
+                break
+
+    def test_classpath(self):
+        env = self.get_newenv()
+        args = self.get_cmdline([launcher, "--print", "-J-cp", some_jar], env=env)
+        it = iter(args)
+        while it:
+            arg = next(it)
+            if arg == "-classpath":
+                self.assertEqual(next(it).split(classpath_delimiter())[-1], some_jar)
+                break
+
+    def test_java_home(self):
+        env = self.get_newenv()
+        my_java = os.path.join(os.sep, "foo", "bar", "my very own (x86) java")
+        env["JAVA_HOME"] = my_java
+        args = self.get_cmdline([launcher, "--print"], env)
+        self.assertEqual(args[0], os.path.join(my_java, "bin", "java"))
+        self.assertEqual(args[1], "-Xmx512m")
+        self.assertEqual(args[2], "-Xss1024k")
+        self.assertEqual(args[-1], "org.python.util.jython")
+
+    def test_java_opts(self):
+        env = self.get_newenv()
+        env["JAVA_OPTS"] = '-Dfoo=bar -Dbaz="some property" -Xmx2g -classpath %s' % some_jar
+        args = self.get_cmdline([launcher, "--print"], env)
+        props = self.get_properties(args)
+        self.assertEqual(args[0], "java")
+        self.assertEqual(args[1], "-Xmx2g")
+        self.assertEqual(args[2], "-Xss1024k")
+        self.assertEqual(args[3], "-classpath", args)
+        self.assertEqual(args[4].split(classpath_delimiter())[-1], some_jar)
+        self.assertEqual(args[-1], "org.python.util.jython")
+        self.assertEqual(props["foo"], "bar")
+        self.assertEqual(props["baz"], "some property")
+
+    def test_default_options(self):
+        env = self.get_newenv()
+        args = self.get_cmdline([launcher, "--print"], env)
+        props = self.get_properties(args)
+        self.assertEqual(args[0], "java")
+        self.assertEqual(args[1], "-Xmx512m")
+        self.assertEqual(args[2], "-Xss1024k")
+        self.assertEqual(args[-1], "org.python.util.jython")
+        self.assertIn("python.home", props)
+        self.assertIn("python.executable", props)
+        self.assertIn("python.launcher.uname", props)
+        self.assertIn("python.launcher.tty", props)
+
+    def test_mem_env(self):
+        env = self.get_newenv()
+        env["JAVA_MEM"] = "-Xmx4g"
+        env["JAVA_STACK"] = "-Xss2m"
+        args = self.get_cmdline([launcher, "--print"], env)
+        self.assertEqual(args[0], "java")
+        self.assertEqual(args[1], "-Xmx4g")
+        self.assertEqual(args[2], "-Xss2m")
+        self.assertEqual(args[-1], "org.python.util.jython")
+
+    def test_mem_options(self):
+        env = self.get_newenv()
+        args = self.get_cmdline([launcher, "-J-Xss2m", "-J-Xmx4g", "--print"], env)
+        self.assertEqual(args[0], "java")
+        self.assertEqual(args[1], "-Xmx4g", args)
+        self.assertEqual(args[2], "-Xss2m", args)
+        self.assertEqual(args[-1], "org.python.util.jython")
+
+    def test_jython_opts_env(self):
+        env = self.get_newenv()
+        env["JYTHON_OPTS"] = '-c "print 47"'
+        args = self.get_cmdline([launcher, "--print"], env)
+        self.assertEqual(args[0], "java")
+        self.assertEqual(args[1], "-Xmx512m")
+        self.assertEqual(args[2], "-Xss1024k")
+        self.assertEqual(args[-3], "org.python.util.jython")
+        self.assertEqual(args[-2], "-c")
+        self.assertEqual(args[-1], "print 47")
+
+    def test_options(self):
+        env = self.get_newenv()
+        args = self.get_cmdline(
+            [launcher,
+             "-Dquoted=a \"quoted\" option",
+             "-Dunder_score=with_underscores",
+             "-Dstarred=*/*/more/*/*",
+             "--print"], env)
+        props = self.get_properties(args)
+        self.assertEqual(props["quoted"], 'a "quoted" option')
+        self.assertEqual(props["under_score"], "with_underscores")
+        self.assertEqual(props["starred"], "*/*/more/*/*")
+
+    def assertHelp(self, output):
+        self.assertIn(
+            "usage: jython [option] ... [-c cmd | -m mod | file | -] [arg] ...",
+            output)
+
+    def test_help(self):
+        self.assertHelp(subprocess.check_output([launcher, "--help"], stderr=subprocess.STDOUT))
+        self.assertHelp(subprocess.check_output([launcher, "--print", "--help"], stderr=subprocess.STDOUT))
+        self.assertHelp(subprocess.check_output([launcher, "--help", "--jdb"], stderr=subprocess.STDOUT))
+        with self.assertRaises(subprocess.CalledProcessError) as cm:
+            subprocess.check_output([launcher, "--bad-arg"], stderr=subprocess.STDOUT)
+        self.assertHelp(cm.exception.output)
+
+    def test_remaining_args(self):
+        env = self.get_newenv()
+        args = self.get_cmdline([launcher, "--print", "--", "--help"], env)
+        self.assertEqual(args[-2], "org.python.util.jython")
+        self.assertEqual(args[-1], "--help")
+
+        args = self.get_cmdline([launcher, "--print", "yolk", "--help"], env)
+        self.assertEqual(args[-3], "org.python.util.jython")
+        self.assertEqual(args[-2], "yolk")
+        self.assertEqual(args[-1], "--help")
+
+    def assertCommand(self, command):
+        args = self.get_cmdline([launcher, "--print"] + command, self.get_newenv())
+        self.assertEqual(args[(len(args) - len(command)):], command)
+
+    def test_file(self):
+        self.assertCommand(['test.py'])
+    
+    def test_dash(self):
+        self.assertCommand(['-i'])
+
+    def test_combined(self):
+        self.assertCommand(['-W', 'action', 'line'])
+
+    def test_singlequoted(self):
+        self.assertCommand(['-c', "'import sys;'"])
+
+    def test_doublequoted(self):
+        self.assertCommand(['-c', '"print \'something\'"'])
+
+    def test_nestedquotes(self):
+        self.assertCommand(['-c', '"print \'something \"really\" cool\'"'])
+
+    def test_nestedquotes2(self):
+        self.assertCommand(['-c', "'print \"something \'really\' cool\"'"])
+
+    def test_starred_args(self):
+        self.assertCommand(["my python command.py", "*/*/my ARGS/*.txt"])
+
+    def test_exclamationmark(self):
+        self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo!', 'ba!r', '!baz', '!', '!!'])
+
+    def test_percentsign(self):
+        self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo%1', 'foo%%1', '%%1bar', '%%1', '%1', '%', '%%'])
+
+    def test_colon(self):
+        self.assertCommand(['-c', 'import sys; print sys.argv[1:]', 'foo:', ':bar'])
+
+    def test_semicolon(self):
+        self.assertCommand(['-c', ';import sys; print sys.argv[1:]', 'foo;'])
+
+
+def test_main():
+    global is_windows
+    global launcher
+    global uname
+
+    if sys.executable is None:
+        return
+    launcher = get_launcher(sys.executable)
+    uname = get_uname()
+    is_windows = uname in ("cygwin", "windows")
+    test_support.run_unittest(
+        TestLauncher)
+
+
+if __name__ == "__main__":
+    test_main()
diff --git a/installer/src/java/org/python/util/install/StartScriptGenerator.java b/installer/src/java/org/python/util/install/StartScriptGenerator.java
--- a/installer/src/java/org/python/util/install/StartScriptGenerator.java
+++ b/installer/src/java/org/python/util/install/StartScriptGenerator.java
@@ -1,10 +1,13 @@
 package org.python.util.install;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.PosixFilePermissions;
 
 
@@ -16,24 +19,51 @@
         _targetDirectory = targetDirectory;
     }
 
-    protected boolean hasCPython27() {
-        int errorCode = 0;
+    protected String getShebang() {
+        String shebang = null;
         try {
             String command[] = new String[]{
                     "/usr/bin/env", "python2.7", "-E",
                     "-c",
                     "import sys; " +
                     "assert sys.version_info.major == 2 and sys.version_info.minor == 7, " +
-                    "'Need Python 2.7, got %r' % (sys.version_info,)"};
+                    "'Need Python 2.7, got %r' % (sys.version_info,);" +
+                    "print sys.executable"};
             long timeout = 3000;
             ChildProcess childProcess = new ChildProcess(command, timeout);
             childProcess.setDebug(false);
             childProcess.setSilent(true);
-            errorCode = childProcess.run();
+            int errorCode = childProcess.run();
+            if (errorCode == 0) {
+                // The whole point of this exercise is that we do not
+                // want the launcher to interpret or otherwise intercept
+                // any PYTHON environment variables that are being passed through.
+                // However, a shebang like /usr/bin/env python2.7 -E
+                // with an extra argument (-E) in general does not work,
+                // such as on Linux, so we have to replace with a hard-coded
+                // path
+                shebang = "#!" + childProcess.getStdout().get(0) + " -E";
+            }
         } catch (Throwable t) {
-            errorCode = 1;
         }
-        return errorCode == 0;
+        return shebang;
+    }
+
+    private final void generateLauncher(String shebang, File infile, File outfile)
+            throws IOException {
+        try (
+                BufferedReader br = new BufferedReader(new FileReader(infile));
+                BufferedWriter bw = new BufferedWriter(new FileWriter(outfile))) {
+            int i = 0;
+            for (String line; (line = br.readLine()) != null; i += 1) {
+                if (i == 0) {
+                    bw.write(shebang);
+                } else {
+                    bw.write(line);
+                }
+                bw.newLine();
+            }
+        }
     }
 
     protected final void generateStartScripts() throws IOException {
@@ -43,12 +73,13 @@
             Files.delete(bindir.resolve("jython.py"));
         }
         else {
-            if (hasCPython27()) {
-                Files.move(bindir.resolve("jython.py"), bindir.resolve("jython"),
-                        StandardCopyOption.REPLACE_EXISTING);
-            } else {
-                Files.delete(bindir.resolve("jython.py"));
+            String shebang = getShebang();
+            if (shebang != null) {
+                generateLauncher(shebang,
+                        bindir.resolve("jython.py").toFile(),
+                        bindir.resolve("jython").toFile());
             }
+            Files.delete(bindir.resolve("jython.py"));
             Files.delete(bindir.resolve("jython.exe"));
             Files.delete(bindir.resolve("python27.dll"));
             Files.setPosixFilePermissions(bindir.resolve("jython"),
diff --git a/src/shell/jython.exe b/src/shell/jython.exe
index 61c68b6657c774bb7d40bcbb3d263411490695eb..2a03827dd99c8d2d908074a2e3de15abf56e30d2
GIT binary patch
[stripped]
diff --git a/src/shell/jython.py b/src/shell/jython.py
--- a/src/shell/jython.py
+++ b/src/shell/jython.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env python2.7 -E
 # -*- coding: utf-8 -*-
 
 # Launch script for Jython. It may be wrapped as an executable with
@@ -7,12 +7,12 @@
 # bin/jython if CPython 2.7 is available with the above shebang
 # invocation.
 
-import argparse
 import glob
 import inspect
 import os
 import os.path
 import pipes
+import shlex
 import subprocess
 import sys
 from collections import OrderedDict
@@ -21,64 +21,69 @@
 is_windows = os.name == "nt" or (os.name == "java" and os._name == "nt")
 
 
-def make_parser(provided_args):
-    parser = argparse.ArgumentParser(description="Jython", add_help=False)
-    parser.add_argument("-D", dest="properties", action="append")
-    parser.add_argument("-J", dest="java", action="append")
-    parser.add_argument("--boot", action="store_true")
-    parser.add_argument("--jdb", action="store_true")
-    parser.add_argument("--help", "-h", action="store_true")
-    parser.add_argument("--print", dest="print_requested", action="store_true")
-    parser.add_argument("--profile", action="store_true")
-    args, remainder = parser.parse_known_args(provided_args)
+def parse_launcher_args(args):
+    class Namespace(object):
+        pass
+    parsed = Namespace()
+    parsed.java = []
+    parsed.properties = OrderedDict()
+    parsed.boot = False
+    parsed.jdb = False
+    parsed.help = False
+    parsed.print_requested = False
+    parsed.profile = False
+    parsed.jdb = None
 
-    items = args.java or []
-    args.java = []
-    for item in items:
-        if item.startswith("-Xmx"):
-            args.mem = item
-        elif item.startswith("-Xss"):
-            args.stack = item
-        else:
-            args.java.append(item)
-    
-    # need to account for the fact that -c and -cp/-classpath are ambiguous options as far
-    # as argparse is concerned, so parse separately
-    args.classpath = []
-    r = iter(remainder)
-    r2 = []
+    it = iter(args)
+    next(it)  # ignore sys.argv[0]
+    i = 1
     while True:
         try:
-            arg = next(r)
+            arg = next(it)
         except StopIteration:
             break
-        if arg == "-cp" or arg == "-classpath":
+        if arg.startswith("-D"):
+            k, v = arg[2:].split("=")
+            parsed.properties[k] = v
+            i += 1
+        elif arg in ("-J-classpath", "-J-cp"):
             try:
-                args.classpath = next(r)
-                if args.classpath.startswith("-"):
-                    parser.error("Invalid classpath for -classpath: %s" % repr(args.classpath)[1:])
+                next_arg = next(it)
             except StopIteration:
-                parser.error("-classpath requires an argument")
+                bad_option("Argument expected for -J-classpath option")
+            if next_arg.startswith("-"):
+                bad_option("Bad option for -J-classpath")
+            parsed.classpath = next_arg
+            i += 2
+        elif arg.startswith("-J-Xmx"):
+            parsed.mem = arg[2:]
+            i += 1
+        elif arg.startswith("-J-Xss"):
+            parsed.stack = arg[2:]
+            i += 1
+        elif arg.startswith("-J"):
+            parsed.java.append(arg[2:])
+            i += 1
+        elif arg == "--print":
+            parsed.print_requested = True
+            i += 1
+        elif arg in ("-h", "--help"):
+            parsed.help = True
+        elif arg in ("--boot", "--jdb", "--profile"):
+            setattr(parsed, arg[2:], True)
+            i += 1
+        elif arg == "--":
+            i += 1
+            break
         else:
-            r2.append(arg)
-    remainder = r2
+            break
 
-    if args.properties is None:
-        args.properties = []
-    props = OrderedDict()
-    for kv in args.properties:
-        k, v = kv.split("=")
-        props[k] = v
-    args.properties = props
-    args.encoding = args.properties.get("file.encoding", None)
-
-    return parser, args
+    return parsed, args[i:]
 
 
 class JythonCommand(object):
 
-    def __init__(self, parser, args, jython_args):
-        self.parser = parser
+    def __init__(self, args, jython_args):
         self.args = args
         self.jython_args = jython_args
 
@@ -109,13 +114,16 @@
         return self._java_command
 
     def setup_java_command(self):
+        if self.args.help:
+            self._java_home = None
+            self._java_command = "java"
+            return
+            
         if "JAVA_HOME" not in os.environ:
             self._java_home = None
             self._java_command = "jdb" if self.args.jdb else "java"
         else:
             self._java_home = os.environ["JAVA_HOME"]
-            #if self.uname == "cygwin":
-            #    self._java_home = subprocess.check_output(["cygpath", "--windows", self._java_home]).strip()
             if self.uname == "cygwin":
                 self._java_command = "jdb" if self.args.jdb else "java"
             else:
@@ -159,51 +167,52 @@
         return ";" if (is_windows or self.uname == "cygwin") else ":"
 
     @property
-    def classpath(self):
-        if hasattr(self, "_classpath"):
-            return self._classpath
+    def jython_jars(self):
+        if hasattr(self, "_jython_jars"):
+            return self._jython_jars
         if os.path.exists(os.path.join(self.jython_home, "jython-dev.jar")):
             jars = [os.path.join(self.jython_home, "jython-dev.jar")]
-            jars.append(os.path.join(self.jython_home, "javalib", "*"))
+            if self.args.boot:
+                # Wildcard expansion does not work for bootclasspath
+                for jar in glob.glob(os.path.join(self.jython_home, "javalib", "*.jar")):
+                    jars.append(jar)
+            else:
+                jars.append(os.path.join(self.jython_home, "javalib", "*"))
         elif not os.path.exists(os.path.join(self.jython_home, "jython.jar")): 
-            self.parser.error(
-"""{executable}:
-{jython_home} contains neither jython-dev.jar nor jython.jar.
+            bad_option("""{jython_home} contains neither jython-dev.jar nor jython.jar.
 Try running this script from the 'bin' directory of an installed Jython or 
-setting {envvar_specifier}JYTHON_HOME.""".\
-                format(
-                    executable=self.executable,
+setting {envvar_specifier}JYTHON_HOME.""".format(
                     jython_home=self.jython_home,
                     envvar_specifier="%" if self.uname == "windows" else "$"))
         else:
             jars = [os.path.join(self.jython_home, "jython.jar")]
-        self._classpath = self.classpath_delimiter.join(jars)
-        if self.args.classpath and not self.args.boot:
-            self._classpath += self.classpath_delimiter + self.args.classpath
-        return self._classpath
+        self._jython_jars = jars
+        return self._jython_jars
+
+    @property
+    def java_classpath(self):
+        if hasattr(self.args, "classpath"):
+            return self.args.classpath
+        else:
+            return os.environ.get("CLASSPATH", ".")
 
     @property
     def java_mem(self):
-        if hasattr(self.args.java, "mem"):
-            return self.args.java.mem
+        if hasattr(self.args, "mem"):
+            return self.args.mem
         else:
             return os.environ.get("JAVA_MEM", "-Xmx512m")
 
     @property
     def java_stack(self):
-        if hasattr(self.args.java, "stack"):
-            return self.args.java.mem
+        if hasattr(self.args, "stack"):
+            return self.args.stack
         else:
             return os.environ.get("JAVA_STACK", "-Xss1024k")
 
     @property
     def java_opts(self):
-        if "JAVA_OPTS" in os.environ:
-            options = os.environ["JAVA_OPTS"].split()
-        else:
-            options = []
-        options.extend([self.java_mem, self.java_stack])
-        return options
+        return [self.java_mem, self.java_stack]
         
     @property
     def java_profile_agent(self):
@@ -219,6 +228,9 @@
         else:
             return arg
 
+    def make_classpath(self, jars):
+        return self.classpath_delimiter.join(jars)
+
     def convert_path(self, arg):
         if self.uname == "cygwin":
             if not arg.startswith("/cygdrive/"):
@@ -235,20 +247,25 @@
         args = [self.java_command]
         args.extend(self.java_opts)
         args.extend(self.args.java)
+
+        classpath = self.java_classpath
+        jython_jars = self.jython_jars
         if self.args.boot:
-            args.append("-Xbootclasspath/a:%s" % self.convert_path(self.classpath))
-            if self.args.classpath:
-                args.extend(["-classpath", self.convert_path(self.args.classpath)])
+            args.append("-Xbootclasspath/a:%s" % self.convert_path(self.make_classpath(jython_jars)))
         else:
-            args.extend(["-classpath", self.convert_path(self.classpath)])
+            classpath = self.make_classpath(jython_jars) + self.classpath_delimiter + classpath
+        args.extend(["-classpath", self.convert_path(classpath)])
+
         if "python.home" not in self.args.properties:
             args.append("-Dpython.home=%s" % self.convert_path(self.jython_home))
         if "python.executable" not in self.args.properties:
             args.append("-Dpython.executable=%s" % self.convert_path(self.executable))
         if "python.launcher.uname" not in self.args.properties:
             args.append("-Dpython.launcher.uname=%s" % self.uname)
-        # determine if is-a-tty for the benefit of running on cygwin - mintty doesn't behave like
-        # a standard windows tty and so JNR posix doesn't detect it properly
+        # Determines whether running on a tty for the benefit of
+        # running on Cygwin. This step is needed because the Mintty
+        # terminal emulator doesn't behave like a standard Microsoft
+        # Windows tty, and so JNR Posix doesn't detect it properly.
         if "python.launcher.tty" not in self.args.properties:
             args.append("-Dpython.launcher.tty=%s" % str(os.isatty(sys.stdin.fileno())).lower())
         if self.uname == "cygwin" and "python.console" not in self.args.properties:
@@ -265,66 +282,141 @@
         return args
 
 
+def bad_option(msg):
+    print >> sys.stderr, """
+{msg}
+usage: jython [option] ... [-c cmd | -m mod | file | -] [arg] ...
+Try `jython -h' for more information.
+""".format(msg=msg)
+    sys.exit(2)
+
+
 def print_help():
     print >> sys.stderr, """
-Jython launcher options:
+Jython launcher-specific options:
+-Dname=value : pass name=value property to Java VM (e.g. -Dpython.path=/a/b/c)
 -Jarg    : pass argument through to Java VM (e.g. -J-Xmx512m)
---jdb    : run under JDB
---print  : print the Java command instead of executing it
+--boot   : speeds up launch performance by putting Jython jars on the boot classpath
+--help   : this help message
+--jdb    : run under JDB java debugger
+--print  : print the Java command with args for launching Jython instead of executing it
 --profile: run with the Java Interactive Profiler (http://jiprof.sf.net)
---boot   : put jython on the boot classpath (disables the bytecode verifier)
 --       : pass remaining arguments through to Jython
 Jython launcher environment variables:
+JAVA_MEM   : Java memory (sets via -Xmx)
+JAVA_OPTS  : options to pass directly to Java
+JAVA_STACK : Java stack size (sets via -Xss)
 JAVA_HOME  : Java installation directory
 JYTHON_HOME: Jython installation directory
 JYTHON_OPTS: default command line arguments
 """
 
+def support_java_opts(args):
+    it = iter(args)
+    while it:
+        arg = next(it)
+        if arg.startswith("-D"):
+            yield arg
+        elif arg in ("-classpath", "-cp"):
+            yield "-J" + arg
+            try:
+                yield next(it)
+            except StopIteration:
+                bad_option("Argument expected for -classpath option in JAVA_OPTS")
+        else:
+            yield "-J" + arg
 
-def split_launcher_args(args):
-    it = iter(args)
-    i = 1
-    next(it)
-    while True:
-        try:
-            arg = next(it)
-        except StopIteration:
-            break
-        if arg.startswith("-D") or arg.startswith("-J") or \
-           arg in ("--boot", "--jdb", "--help", "--print", "--profile"):
-            i += 1
-        elif arg in ("-cp", "-classpath"):
-            i += 1
-            try:
-                next(it)
-                i += 1
-            except StopIteration:
-                break  # will be picked up in argparse, where an error will be raised
-        elif arg == "--":
-            i += 1
-            break
+
+# copied from subprocess module in Jython; see
+# http://bugs.python.org/issue1724822 where it is discussed to include
+# in Python 3.x for shlex:
+def cmdline2list(cmdline):
+    """Build an argv list from a Microsoft shell style cmdline str
+
+    The reverse of list2cmdline that follows the same MS C runtime
+    rules.
+    """
+    whitespace = ' \t'
+    # count of preceding '\'
+    bs_count = 0
+    in_quotes = False
+    arg = []
+    argv = []
+
+    for ch in cmdline:
+        if ch in whitespace and not in_quotes:
+            if arg:
+                # finalize arg and reset
+                argv.append(''.join(arg))
+                arg = []
+            bs_count = 0
+        elif ch == '\\':
+            arg.append(ch)
+            bs_count += 1
+        elif ch == '"':
+            if not bs_count % 2:
+                # Even number of '\' followed by a '"'. Place one
+                # '\' for every pair and treat '"' as a delimiter
+                if bs_count:
+                    del arg[-(bs_count / 2):]
+                in_quotes = not in_quotes
+            else:
+                # Odd number of '\' followed by a '"'. Place one '\'
+                # for every pair and treat '"' as an escape sequence
+                # by the remaining '\'
+                del arg[-(bs_count / 2 + 1):]
+                arg.append(ch)
+            bs_count = 0
         else:
-            break
-    return args[:i], args[i:]
+            # regular char
+            arg.append(ch)
+            bs_count = 0
 
+    # A single trailing '"' delimiter yields an empty arg
+    if arg or in_quotes:
+        argv.append(''.join(arg))
 
-def main():
+    return argv
+
+
+def decode_args(sys_args):
+    args = [sys_args[0]]
+
+    def get_env_opts(envvar):
+        opts = os.environ.get(envvar, "")
+        if is_windows:
+            return cmdline2list(opts)
+        else:
+            return shlex.split(opts)
+
+    java_opts = get_env_opts("JAVA_OPTS")
+    jython_opts = get_env_opts("JYTHON_OPTS")
+
+    args.extend(support_java_opts(java_opts))
+    args.extend(sys_args[1:])
+
     if sys.stdout.encoding:
         if sys.stdout.encoding.lower() == "cp65001":
             sys.exit("""Jython does not support code page 65001 (CP_UTF8).
 Please try another code page by setting it with the chcp command.""")
-        sys.argv = [arg.decode(sys.stdout.encoding) for arg in sys.argv]
-    launcher_args, jython_args = split_launcher_args(sys.argv)
-    parser, args = make_parser(launcher_args)
-    jython_command = JythonCommand(parser, args, jython_args)
+        args = [arg.decode(sys.stdout.encoding) for arg in args]
+        jython_opts = [arg.decode(sys.stdout.encoding) for arg in jython_opts]
+
+    return args, jython_opts
+
+
+def main(sys_args):
+    sys_args, jython_opts = decode_args(sys_args)
+    args, jython_args = parse_launcher_args(sys_args)
+    jython_command = JythonCommand(args, jython_opts + jython_args)
     command = jython_command.command
 
-    if args.profile:
+    if args.profile and not args.help:
         try:
             os.unlink("profile.txt")
         except OSError:
             pass
-    if args.print_requested:
+    if args.print_requested and not args.help:
         if jython_command.uname == "windows":
             print subprocess.list2cmdline(jython_command.command)
         else:
@@ -349,4 +441,4 @@
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv)

-- 
Repository URL: https://hg.python.org/jython


More information about the Jython-checkins mailing list