Wed, 11 May 2016 14:21:52 +0200
8144221: fix Nashorn shebang argument handling on Mac/Linux
Reviewed-by: jlaskey, lagergren
1.1 --- a/make/build.xml Wed May 11 14:21:44 2016 +0200 1.2 +++ b/make/build.xml Wed May 11 14:21:52 2016 +0200 1.3 @@ -83,6 +83,12 @@ 1.4 <condition property="jfr.options" value="${run.test.jvmargs.jfr}" else=""> 1.5 <istrue value="${jfr}"/> 1.6 </condition> 1.7 + 1.8 + <condition property="test-sys-prop-no-security.os.not.windows"> 1.9 + <not> 1.10 + <os family="windows"/> 1.11 + </not> 1.12 + </condition> 1.13 </target> 1.14 1.15 <!-- check minimum ant version required to be 1.8.4 -->
2.1 --- a/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java Wed May 11 14:21:44 2016 +0200 2.2 +++ b/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java Wed May 11 14:21:52 2016 +0200 2.3 @@ -278,9 +278,8 @@ 2.4 * @param str a {@link String} to tokenize. 2.5 * @return a {@link List} of {@link String}s representing the tokens that 2.6 * constitute the string. 2.7 - * @throws IOException in case {@link StreamTokenizer#nextToken()} raises it. 2.8 */ 2.9 - public static List<String> tokenizeString(final String str) throws IOException { 2.10 + public static List<String> tokenizeString(final String str) { 2.11 final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str)); 2.12 tokenizer.resetSyntax(); 2.13 tokenizer.wordChars(0, 255); 2.14 @@ -290,7 +289,7 @@ 2.15 tokenizer.quoteChar('\''); 2.16 final List<String> tokenList = new ArrayList<>(); 2.17 final StringBuilder toAppend = new StringBuilder(); 2.18 - while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) { 2.19 + while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) { 2.20 final String s = tokenizer.sval; 2.21 // The tokenizer understands about honoring quoted strings and recognizes 2.22 // them as one token that possibly contains multiple space-separated words. 2.23 @@ -309,4 +308,12 @@ 2.24 } 2.25 return tokenList; 2.26 } 2.27 + 2.28 + private static int nextToken(final StreamTokenizer tokenizer) { 2.29 + try { 2.30 + return tokenizer.nextToken(); 2.31 + } catch (final IOException ioe) { 2.32 + return StreamTokenizer.TT_EOF; 2.33 + } 2.34 + } 2.35 }
3.1 --- a/src/jdk/nashorn/tools/Shell.java Wed May 11 14:21:44 2016 +0200 3.2 +++ b/src/jdk/nashorn/tools/Shell.java Wed May 11 14:21:52 2016 +0200 3.3 @@ -25,7 +25,23 @@ 3.4 3.5 package jdk.nashorn.tools; 3.6 3.7 -import static jdk.nashorn.internal.runtime.Source.sourceFor; 3.8 +import jdk.nashorn.api.scripting.NashornException; 3.9 +import jdk.nashorn.internal.codegen.Compiler; 3.10 +import jdk.nashorn.internal.codegen.Compiler.CompilationPhases; 3.11 +import jdk.nashorn.internal.ir.FunctionNode; 3.12 +import jdk.nashorn.internal.ir.debug.ASTWriter; 3.13 +import jdk.nashorn.internal.ir.debug.PrintVisitor; 3.14 +import jdk.nashorn.internal.objects.Global; 3.15 +import jdk.nashorn.internal.parser.Parser; 3.16 +import jdk.nashorn.internal.runtime.Context; 3.17 +import jdk.nashorn.internal.runtime.ErrorManager; 3.18 +import jdk.nashorn.internal.runtime.JSType; 3.19 +import jdk.nashorn.internal.runtime.Property; 3.20 +import jdk.nashorn.internal.runtime.ScriptEnvironment; 3.21 +import jdk.nashorn.internal.runtime.ScriptFunction; 3.22 +import jdk.nashorn.internal.runtime.ScriptRuntime; 3.23 +import jdk.nashorn.internal.runtime.ScriptingFunctions; 3.24 +import jdk.nashorn.internal.runtime.options.Options; 3.25 3.26 import java.io.BufferedReader; 3.27 import java.io.File; 3.28 @@ -45,22 +61,8 @@ 3.29 import java.util.List; 3.30 import java.util.Locale; 3.31 import java.util.ResourceBundle; 3.32 -import jdk.nashorn.api.scripting.NashornException; 3.33 -import jdk.nashorn.internal.codegen.Compiler; 3.34 -import jdk.nashorn.internal.codegen.Compiler.CompilationPhases; 3.35 -import jdk.nashorn.internal.ir.FunctionNode; 3.36 -import jdk.nashorn.internal.ir.debug.ASTWriter; 3.37 -import jdk.nashorn.internal.ir.debug.PrintVisitor; 3.38 -import jdk.nashorn.internal.objects.Global; 3.39 -import jdk.nashorn.internal.parser.Parser; 3.40 -import jdk.nashorn.internal.runtime.Context; 3.41 -import jdk.nashorn.internal.runtime.ErrorManager; 3.42 -import jdk.nashorn.internal.runtime.JSType; 3.43 -import jdk.nashorn.internal.runtime.Property; 3.44 -import jdk.nashorn.internal.runtime.ScriptEnvironment; 3.45 -import jdk.nashorn.internal.runtime.ScriptFunction; 3.46 -import jdk.nashorn.internal.runtime.ScriptRuntime; 3.47 -import jdk.nashorn.internal.runtime.options.Options; 3.48 + 3.49 +import static jdk.nashorn.internal.runtime.Source.sourceFor; 3.50 3.51 /** 3.52 * Command line Shell for processing JavaScript files. 3.53 @@ -201,8 +203,7 @@ 3.54 // parse options 3.55 if (args != null) { 3.56 try { 3.57 - // FIXME: preprocessArgs does not yet work fine 3.58 - final String[] prepArgs = args; // preprocessArgs(args); 3.59 + final String[] prepArgs = preprocessArgs(args); 3.60 options.process(prepArgs); 3.61 } catch (final IllegalArgumentException e) { 3.62 werr.println(bundle.getString("shell.usage")); 3.63 @@ -234,35 +235,53 @@ 3.64 } 3.65 3.66 /** 3.67 - * Preprocess the command line arguments passed in by the shell. This checks, for each of the arguments, whether it 3.68 - * can be a file name, and if so, whether the file exists. If the file exists and begins with a shebang line, and 3.69 - * the arguments on that line are a prefix of {@code args} with the file removed, it is assumed that a script file 3.70 - * being executed via shebang was found, and it is moved to the appropriate position in the argument list. The first 3.71 - * such match is used. 3.72 + * Preprocess the command line arguments passed in by the shell. This method checks, for the first non-option 3.73 + * argument, whether the file denoted by it begins with a shebang line. If so, it is assumed that execution in 3.74 + * shebang mode is intended. The consequence of this is that the identified script file will be treated as the 3.75 + * <em>only</em> script file, and all subsequent arguments will be regarded as arguments to the script. 3.76 * <p> 3.77 - * This method canonicalizes the command line arguments to the form {@code <options> <scripts> -- <arguments>}, 3.78 - * where the last of the {@code scripts} is the one being run in shebang fashion. 3.79 + * This method canonicalizes the command line arguments to the form {@code <options> <script> -- <arguments>} if a 3.80 + * shebang script is identified. On platforms that pass shebang arguments as single strings, the shebang arguments 3.81 + * will be broken down into single arguments; whitespace is used as separator. 3.82 + * <p> 3.83 + * Shebang mode is entered regardless of whether the script is actually run directly from the shell, or indirectly 3.84 + * via the {@code jjs} executable. It is the user's / script author's responsibility to ensure that the arguments 3.85 + * given on the shebang line do not lead to a malformed argument sequence. In particular, the shebang arguments 3.86 + * should not contain any whitespace for purposes other than separating arguments, as the different platforms deal 3.87 + * with whitespace in different and incompatible ways. 3.88 * <p> 3.89 * @implNote Example:<ul> 3.90 - * <li>Shebang line in {@code script.js}: {@code #!/path/to/jjs --language=es6 other.js -- arg1}</li> 3.91 + * <li>Shebang line in {@code script.js}: {@code #!/path/to/jjs --language=es6}</li> 3.92 * <li>Command line: {@code ./script.js arg2}</li> 3.93 - * <li>{@code args} array passed to Nashorn: {@code --language=es6,other.js,--,arg1,./script.js,arg2}</li> 3.94 - * <li>Required canonicalized arguments array: {@code --language=es6,other.js,./script.js,--,arg1,arg2}</li> 3.95 + * <li>{@code args} array passed to Nashorn: {@code --language=es6,./script.js,arg}</li> 3.96 + * <li>Required canonicalized arguments array: {@code --language=es6,./script.js,--,arg2}</li> 3.97 * </ul> 3.98 * 3.99 * @param args the command line arguments as passed into Nashorn. 3.100 - * @return a properly ordered argument list 3.101 + * @return the passed and possibly canonicalized argument list 3.102 */ 3.103 private static String[] preprocessArgs(final String[] args) { 3.104 - final List<String> largs = new ArrayList<>(); 3.105 - Collections.addAll(largs, args); 3.106 - final List<String> pa = new ArrayList<>(); 3.107 - String scriptFile = null; 3.108 - boolean found = false; 3.109 - for (int i = 0; i < args.length; ++i) { 3.110 - final String a = args[i]; 3.111 - final Path p = Paths.get(a); 3.112 - if (!found && (!a.startsWith("-") || a.length() == 1) && Files.exists(p)) { 3.113 + if (args.length == 0) { 3.114 + return args; 3.115 + } 3.116 + 3.117 + final List<String> processedArgs = new ArrayList<>(); 3.118 + processedArgs.addAll(Arrays.asList(args)); 3.119 + 3.120 + // Nashorn supports passing multiple shebang arguments. On platforms that pass anything following the 3.121 + // shebang interpreter notice as one argument, the first element of the argument array needs to be special-cased 3.122 + // as it might actually contain several arguments. Mac OS X splits shebang arguments, other platforms don't. 3.123 + // This special handling is also only necessary if the first argument actually starts with an option. 3.124 + if (args[0].startsWith("-") && !System.getProperty("os.name", "generic").startsWith("Mac OS X")) { 3.125 + processedArgs.addAll(0, ScriptingFunctions.tokenizeString(processedArgs.remove(0))); 3.126 + } 3.127 + 3.128 + int shebangFilePos = -1; // -1 signifies "none found" 3.129 + // identify a shebang file and its position in the arguments array (if any) 3.130 + for (int i = 0; i < processedArgs.size(); ++i) { 3.131 + final String a = processedArgs.get(i); 3.132 + if (!a.startsWith("-")) { 3.133 + final Path p = Paths.get(a); 3.134 String l = ""; 3.135 try (final BufferedReader r = Files.newBufferedReader(p)) { 3.136 l = r.readLine(); 3.137 @@ -270,40 +289,18 @@ 3.138 // ignore 3.139 } 3.140 if (l.startsWith("#!")) { 3.141 - List<String> shebangArgs = Arrays.asList(l.split(" ")); 3.142 - shebangArgs = shebangArgs.subList(1, shebangArgs.size()); // remove #! part 3.143 - final int ssize = shebangArgs.size(); 3.144 - final List<String> filteredArgs = new ArrayList<>(); 3.145 - for (final String x : largs) { 3.146 - if (!x.equals(a)) { 3.147 - filteredArgs.add(x); 3.148 - } 3.149 - } 3.150 - if (filteredArgs.size() >= ssize && shebangArgs.equals(filteredArgs.subList(0, ssize))) { 3.151 - scriptFile = a; 3.152 - found = true; 3.153 - continue; 3.154 - } 3.155 + shebangFilePos = i; 3.156 } 3.157 + // We're only checking the first non-option argument. If it's not a shebang file, we're in normal 3.158 + // execution mode. 3.159 + break; 3.160 } 3.161 - pa.add(a); 3.162 } 3.163 - if (scriptFile != null) { 3.164 - // Insert the found script file name either before a -- argument, or at the end of the options list, before 3.165 - // any other arguments, with an extra --. 3.166 - int argidx = pa.indexOf("--"); 3.167 - if (argidx == -1) { 3.168 - for (String s : pa) { 3.169 - ++argidx; 3.170 - if (s.charAt(0) != '-') { 3.171 - pa.add(argidx, "--"); 3.172 - break; 3.173 - } 3.174 - } 3.175 - } 3.176 - pa.add(argidx, scriptFile); 3.177 + if (shebangFilePos != -1) { 3.178 + // Insert the argument separator after the shebang script file. 3.179 + processedArgs.add(shebangFilePos + 1, "--"); 3.180 } 3.181 - return pa.toArray(new String[0]); 3.182 + return processedArgs.toArray(new String[0]); 3.183 } 3.184 3.185 /**
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/test/script/nosecurity/JDK-8144221.js Wed May 11 14:21:52 2016 +0200 4.3 @@ -0,0 +1,155 @@ 4.4 +/* 4.5 + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 4.6 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4.7 + * 4.8 + * This code is free software; you can redistribute it and/or modify it 4.9 + * under the terms of the GNU General Public License version 2 only, as 4.10 + * published by the Free Software Foundation. 4.11 + * 4.12 + * This code is distributed in the hope that it will be useful, but WITHOUT 4.13 + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 4.14 + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 4.15 + * version 2 for more details (a copy is included in the LICENSE file that 4.16 + * accompanied this code). 4.17 + * 4.18 + * You should have received a copy of the GNU General Public License version 4.19 + * 2 along with this work; if not, write to the Free Software Foundation, 4.20 + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 4.21 + * 4.22 + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 4.23 + * or visit www.oracle.com if you need additional information or have any 4.24 + * questions. 4.25 + */ 4.26 + 4.27 +/** 4.28 + * Test that shebang handling works properly. 4.29 + * 4.30 + * @test 4.31 + * @runif os.not.windows 4.32 + * @option -scripting 4.33 + * @run 4.34 + */ 4.35 + 4.36 +// The test generates three different JavaScript source files. The first two 4.37 +// are generated at the beginning of the test and do not change. 4.38 +// * a.js 4.39 +// print("A: " + arguments) 4.40 +// * b.js 4.41 +// #!<path_to_jjs> -lalelu -- ignore 4.42 +// print("B: " + arguments) 4.43 +// 4.44 +// The third file, shebang.js, is generated differently for each particular 4.45 +// test case, containing different shebang lines and one statement: 4.46 +// * shebang.js 4.47 +// #!<path_to_jjs> <shebang_line> 4.48 +// print("S: " + arguments) 4.49 +// 4.50 +// The path_to_jjs is extracted from the environment based on JAVA_HOME, so the 4.51 +// latter must be set properly. 4.52 +// 4.53 +// Each shebang.js is run four times, in all possible combinations of values 4.54 +// from the following two axes: 4.55 +// * without passing any arguments, and passing the arguments 'a.js' and 4.56 +// '"hello world"' (the latter being a quoted string); 4.57 +// * run via jjs, and via direct shell execution (using shebang). 4.58 + 4.59 +var pseudosheb = "#!${jjs} -lalelu -- ignore", 4.60 + System = Java.type('java.lang.System'), 4.61 + Paths = Java.type('java.nio.file.Paths'), 4.62 + Files = Java.type('java.nio.file.Files'), 4.63 + Opt = Java.type('java.nio.file.StandardOpenOption'), 4.64 + Arrays = Java.type('java.util.Arrays') 4.65 + 4.66 +var sep = Java.type('java.io.File').separator, 4.67 + win = System.getProperty("os.name").startsWith("Windows"), 4.68 + jjsName = "jjs" + (win ? ".exe" : ""), 4.69 + javaHome = System.getProperty("java.home") 4.70 + 4.71 +var jjs = javaHome + "/../bin/".replace(/\//g, sep) + jjsName 4.72 +if (!Files.exists(Paths.get(jjs))) { 4.73 + jjs = javaHome + "/bin/".replace(/\//g, sep) + jjsName 4.74 +} 4.75 + 4.76 +// Create and cwd to a temporary directory. 4.77 + 4.78 +var tmpdir = Files.createTempDirectory(null), 4.79 + tmp = tmpdir.toAbsolutePath().toString(), 4.80 + curpwd = $ENV.PWD 4.81 + 4.82 +$ENV.PWD = tmp 4.83 + 4.84 +// Test cases. Each case is documented with the expected output for the four 4.85 +// different executions. 4.86 + 4.87 +var shebs = [ 4.88 + // No arguments on the shebang line. 4.89 + // noargs jjs/shebang -> no output but "S" prefix 4.90 + // args jjs/shebang -> output the arguments with "S" prefix 4.91 + "", 4.92 + // One interpreter argument. 4.93 + // noargs jjs/shebang -> no output but "S" prefix 4.94 + // args jjs/shebang -> output the arguments with "S" prefix 4.95 + "--language=es6", 4.96 + // Two interpreter arguments. 4.97 + // noargs jjs/shebang -> no output but "S" prefix 4.98 + // args jjs/shebang -> output the arguments with "S" prefix 4.99 + "--language=es6 -scripting", 4.100 + // One interpreter argument and a JavaScript file without shebang. 4.101 + // (For shebang execution, this is a pathological example, as the 4.102 + // JavaScript file passed as a shebang argument will be analyzed and 4.103 + // shebang mode will not be entered.) 4.104 + // noargs jjs -> no output but "S" prefix 4.105 + // args jjs -> output the arguments with "S" prefix 4.106 + // noargs shebang -> no output but "A" and "S" prefixes 4.107 + // args shebang -> output "A", "S", and "A" prefixes, then the error 4.108 + // message: 4.109 + // "java.io.IOException: hello world is not a file" 4.110 + "-scripting a.js", 4.111 + // One interpreter argument and a JavaScript file with shebang. (This 4.112 + // is another pathological example, as it will force shebang mode, 4.113 + // leading to all subsequent arguments, including shebang.js, being 4.114 + // treated as arguments to the script b.js.) 4.115 + // noargs jjs -> no output but the "S" prefix 4.116 + // args jjs -> output the arguments with "S" prefix 4.117 + // noargs shebang -> output shebang.js with "B" prefix 4.118 + // args shebang -> output shebang.js and the arguments with "B" 4.119 + // prefix 4.120 + "-scripting b.js" 4.121 + ] 4.122 + 4.123 +function write(file, lines) { 4.124 + Files.write(Paths.get(tmp, file), Arrays.asList(lines), Opt.CREATE, Opt.WRITE) 4.125 +} 4.126 + 4.127 +function insn(name) { 4.128 + return "print('${name}:' + arguments)" 4.129 +} 4.130 + 4.131 +function run(viajjs, name, arg1, arg2) { 4.132 + var prefix = viajjs ? "${jjs} -scripting " : '' 4.133 + $EXEC("${prefix}./shebang.js ${arg1} ${arg2}") 4.134 + print("* ${name} via ${viajjs ? 'jjs' : 'shebang'}") 4.135 + print($OUT.trim()) 4.136 + print($ERR.trim()) 4.137 +} 4.138 + 4.139 +write('a.js', insn('A')) 4.140 +write('b.js', [pseudosheb, insn('B')]) 4.141 + 4.142 +shebs.forEach(function(sheb) { 4.143 + var shebang = "#!${jjs} ${sheb}" 4.144 + print("<<< ${sheb} >>>") 4.145 + write('shebang.js', [shebang, insn('S')]) 4.146 + $EXEC('chmod +x shebang.js') 4.147 + run(false, 'noargs', '', '') 4.148 + run(true, 'noargs', '', '') 4.149 + run(false, 'withargs', 'a.js', '"hello world"') 4.150 + run(true, 'withargs', 'a.js', '"hello world"') 4.151 + $EXEC('rm shebang.js') 4.152 +}) 4.153 + 4.154 +// Cleanup. 4.155 + 4.156 +$EXEC('rm a.js b.js') 4.157 +$ENV.PWD = curpwd 4.158 +Files.delete(tmpdir)
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/test/script/nosecurity/JDK-8144221.js.EXPECTED Wed May 11 14:21:52 2016 +0200 5.3 @@ -0,0 +1,68 @@ 5.4 +<<< >>> 5.5 +* noargs via shebang 5.6 +S: 5.7 + 5.8 +* noargs via jjs 5.9 +S: 5.10 + 5.11 +* withargs via shebang 5.12 +S:a.js,hello world 5.13 + 5.14 +* withargs via jjs 5.15 +S:a.js,hello world 5.16 + 5.17 +<<< --language=es6 >>> 5.18 +* noargs via shebang 5.19 +S: 5.20 + 5.21 +* noargs via jjs 5.22 +S: 5.23 + 5.24 +* withargs via shebang 5.25 +S:a.js,hello world 5.26 + 5.27 +* withargs via jjs 5.28 +S:a.js,hello world 5.29 + 5.30 +<<< --language=es6 -scripting >>> 5.31 +* noargs via shebang 5.32 +S: 5.33 + 5.34 +* noargs via jjs 5.35 +S: 5.36 + 5.37 +* withargs via shebang 5.38 +S:a.js,hello world 5.39 + 5.40 +* withargs via jjs 5.41 +S:a.js,hello world 5.42 + 5.43 +<<< -scripting a.js >>> 5.44 +* noargs via shebang 5.45 +A: 5.46 +S: 5.47 + 5.48 +* noargs via jjs 5.49 +S: 5.50 + 5.51 +* withargs via shebang 5.52 +A: 5.53 +S: 5.54 +A: 5.55 +java.io.IOException: hello world is not a file 5.56 +* withargs via jjs 5.57 +S:a.js,hello world 5.58 + 5.59 +<<< -scripting b.js >>> 5.60 +* noargs via shebang 5.61 +B:./shebang.js 5.62 + 5.63 +* noargs via jjs 5.64 +S: 5.65 + 5.66 +* withargs via shebang 5.67 +B:./shebang.js,a.js,hello world 5.68 + 5.69 +* withargs via jjs 5.70 +S:a.js,hello world 5.71 +
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/test/script/nosecurity/os-not-windows.js Wed May 11 14:21:52 2016 +0200 6.3 @@ -0,0 +1,37 @@ 6.4 +/* 6.5 + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 6.6 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6.7 + * 6.8 + * This code is free software; you can redistribute it and/or modify it 6.9 + * under the terms of the GNU General Public License version 2 only, as 6.10 + * published by the Free Software Foundation. 6.11 + * 6.12 + * This code is distributed in the hope that it will be useful, but WITHOUT 6.13 + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 6.14 + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 6.15 + * version 2 for more details (a copy is included in the LICENSE file that 6.16 + * accompanied this code). 6.17 + * 6.18 + * You should have received a copy of the GNU General Public License version 6.19 + * 2 along with this work; if not, write to the Free Software Foundation, 6.20 + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 6.21 + * 6.22 + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 6.23 + * or visit www.oracle.com if you need additional information or have any 6.24 + * questions. 6.25 + */ 6.26 + 6.27 +/** 6.28 + * Test that we're not running on Windows. The test actually checks if the os.not.windows property is set and processed 6.29 + * by runif correctly. 6.30 + * 6.31 + * @test 6.32 + * @runif os.not.windows 6.33 + * @run 6.34 + */ 6.35 + 6.36 +var os = java.lang.System.getProperty("os.name") 6.37 + 6.38 +if (os.startsWith("Windows")) { 6.39 + throw "This test should not be run on Windows." 6.40 +}