# HG changeset patch # User jlaskey # Date 1360003715 14400 # Node ID 6f58c28c4faa0d3f661f33e9f6883eeaa1f526b4 # Parent bee7c8a45a0498c21218d2ff59d70f3090714f91 8006191: `cmd` -> exec("cmd") in script mode Reviewed-by: sundar, lagergren, hannesw Contributed-by: james.laskey@oracle.com diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/api/scripting/NashornScriptEngine.java --- a/src/jdk/nashorn/api/scripting/NashornScriptEngine.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/api/scripting/NashornScriptEngine.java Mon Feb 04 14:48:35 2013 -0400 @@ -259,7 +259,7 @@ @Override public ScriptObject run() { try { - return nashornContext.createGlobal(); + return nashornContext.newGlobal(); } catch (final RuntimeException e) { if (Context.DEBUG) { e.printStackTrace(); @@ -269,6 +269,8 @@ } }); + nashornContext.initGlobal(newGlobal); + // current ScriptContext exposed as "context" newGlobal.addOwnProperty("context", Property.NOT_ENUMERABLE, UNDEFINED); // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/objects/Global.java --- a/src/jdk/nashorn/internal/objects/Global.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/objects/Global.java Mon Feb 04 14:48:35 2013 -0400 @@ -1450,6 +1450,10 @@ value = ScriptFunctionImpl.makeFunction("quit", ScriptingFunctions.QUIT); addOwnProperty("quit", Attribute.NOT_ENUMERABLE, value); + final String execName = ScriptingFunctions.EXEC_NAME; + value = ScriptFunctionImpl.makeFunction(execName, ScriptingFunctions.EXEC); + addOwnProperty(execName, Attribute.NOT_ENUMERABLE, value); + // Nashorn extension: global.echo (scripting-mode-only) // alias for "print" value = get("print"); @@ -1458,6 +1462,10 @@ // Nashorn extension: global.$OPTIONS (scripting-mode-only) value = new OptionsObject(this.getContext()); addOwnProperty("$OPTIONS", Attribute.NOT_ENUMERABLE, value); + + // Nashorn extension: global.$ENV (scripting-mode-only) + value = ScriptingFunctions.getENVValues(newEmptyInstance(), this.isStrictContext()); + addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, value); } private void initTypedArray() { diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/parser/Lexer.java --- a/src/jdk/nashorn/internal/parser/Lexer.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/parser/Lexer.java Mon Feb 04 14:48:35 2013 -0400 @@ -36,9 +36,11 @@ import static jdk.nashorn.internal.parser.TokenType.LBRACE; import static jdk.nashorn.internal.parser.TokenType.LPAREN; import static jdk.nashorn.internal.parser.TokenType.OCTAL; +import static jdk.nashorn.internal.parser.TokenType.RBRACE; import static jdk.nashorn.internal.parser.TokenType.REGEX; import static jdk.nashorn.internal.parser.TokenType.RPAREN; import static jdk.nashorn.internal.parser.TokenType.STRING; +import static jdk.nashorn.internal.parser.TokenType.EXECSTRING; import static jdk.nashorn.internal.parser.TokenType.XML; import jdk.nashorn.internal.runtime.ECMAErrors; @@ -367,12 +369,13 @@ } /** - * Test if char is a string delimiter, e.g. '\' or '"' + * Test if char is a string delimiter, e.g. '\' or '"'. Also scans exec + * strings ('`') in scripting mode. * @param ch a char * @return true if string delimiter */ protected boolean isStringDelimiter(final char ch) { - return ch == '\'' || ch == '"'; + return ch == '\'' || ch == '"' || (scripting && ch == '`'); } /** @@ -936,12 +939,29 @@ // Record end of string. stringState.setLimit(position - 1); - // Only edit double quoted strings. - if (scripting && quote == '\"' && !stringState.isEmpty()) { - // Edit string. - editString(type, stringState); + if (scripting && !stringState.isEmpty()) { + switch (quote) { + case '`': + // Mark the beginning of an exec string. + add(EXECSTRING, stringState.position, stringState.limit); + // Frame edit string with left brace. + add(LBRACE, stringState.position, stringState.position); + // Process edit string. + editString(type, stringState); + // Frame edit string with right brace. + add(RBRACE, stringState.limit, stringState.limit); + break; + case '"': + // Only edit double quoted strings. + editString(type, stringState); + break; + case '\'': + // Add string token without editing. + add(type, stringState.position, stringState.limit); + break; + } } else { - // Add string token. + /// Add string token without editing. add(type, stringState.position, stringState.limit); } } diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/parser/Parser.java --- a/src/jdk/nashorn/internal/parser/Parser.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/parser/Parser.java Mon Feb 04 14:48:35 2013 -0400 @@ -39,6 +39,7 @@ import static jdk.nashorn.internal.parser.TokenType.ELSE; import static jdk.nashorn.internal.parser.TokenType.EOF; import static jdk.nashorn.internal.parser.TokenType.EOL; +import static jdk.nashorn.internal.parser.TokenType.EXECSTRING; import static jdk.nashorn.internal.parser.TokenType.FINALLY; import static jdk.nashorn.internal.parser.TokenType.FUNCTION; import static jdk.nashorn.internal.parser.TokenType.IDENT; @@ -98,6 +99,7 @@ import jdk.nashorn.internal.runtime.Context; import jdk.nashorn.internal.runtime.JSErrorType; import jdk.nashorn.internal.runtime.ParserException; +import jdk.nashorn.internal.runtime.ScriptingFunctions; /** * Builds the IR. @@ -107,8 +109,12 @@ /** Code generator. */ private final Compiler compiler; + /** Current context. */ private final Context context; + /** Is scripting mode. */ + private final boolean scripting; + /** Top level script being compiled. */ private FunctionNode script; @@ -136,6 +142,7 @@ this.compiler = compiler; this.context = compiler.getContext(); + this.scripting = this.context._scripting; } /** @@ -146,7 +153,7 @@ public FunctionNode parse(final String scriptName) { try { stream = new TokenStream(); - lexer = new Lexer(source, stream, context._scripting && !context._no_syntax_extensions); + lexer = new Lexer(source, stream, scripting && !context._no_syntax_extensions); // Set up first token (skips opening EOL.) k = -1; @@ -1856,6 +1863,8 @@ case REGEX: case XML: return getLiteral(); + case EXECSTRING: + return execString(primaryToken); case FALSE: next(); return LiteralNode.newInstance(source, primaryToken, finish, false); @@ -1893,6 +1902,28 @@ return null; } + /** + * Convert execString to a call to $EXEC. + * + * @param primaryToken Original string token. + * @return callNode to $EXEC. + */ + Node execString(final long primaryToken) { + // Synthesize an ident to call $EXEC. + final IdentNode execIdent = new IdentNode(source, primaryToken, finish, ScriptingFunctions.EXEC_NAME); + // Skip over EXECSTRING. + next(); + // Set up argument list for call. + final List arguments = new ArrayList<>(); + // Skip beginning of edit string expression. + expect(LBRACE); + // Add the following expression to arguments. + arguments.add(expression()); + // Skip ending of edit string expression. + expect(RBRACE); + + return new CallNode(source, primaryToken, finish, execIdent, arguments); + } /** * ArrayLiteral : diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/parser/TokenType.java --- a/src/jdk/nashorn/internal/parser/TokenType.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/parser/TokenType.java Mon Feb 04 14:48:35 2013 -0400 @@ -168,6 +168,7 @@ FLOATING (LITERAL, null), STRING (LITERAL, null), ESCSTRING (LITERAL, null), + EXECSTRING (LITERAL, null), IDENT (LITERAL, null), REGEX (LITERAL, null), XML (LITERAL, null), diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/runtime/Context.java --- a/src/jdk/nashorn/internal/runtime/Context.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/runtime/Context.java Mon Feb 04 14:48:35 2013 -0400 @@ -744,16 +744,37 @@ } /** - * Create global script object + * Create and initialize a new global scope object. + * + * @return the initialized global scope object. + */ + public ScriptObject createGlobal() { + return initGlobal(newGlobal()); + } + + /** + * Create a new uninitialized global scope object * @return the global script object */ - public ScriptObject createGlobal() { + public ScriptObject newGlobal() { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("createNashornGlobal")); } - final ScriptObject global = newGlobal(); + return newGlobalTrusted(); + } + + /** + * Initialize given global scope object. + * + * @return the initialized global scope object. + */ + public ScriptObject initGlobal(final ScriptObject global) { + if (! (global instanceof GlobalObject)) { + throw new IllegalArgumentException("not a global object!"); + } + // Need only minimal global object, if we are just compiling. if (!_compile_only) { final ScriptObject oldGlobal = Context.getGlobalTrusted(); @@ -929,7 +950,7 @@ }); } - private ScriptObject newGlobal() { + private ScriptObject newGlobalTrusted() { try { final Class clazz = Class.forName("jdk.nashorn.internal.objects.Global", true, scriptLoader); return (ScriptObject) clazz.newInstance(); diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/runtime/OptionsObject.java --- a/src/jdk/nashorn/internal/runtime/OptionsObject.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/runtime/OptionsObject.java Mon Feb 04 14:48:35 2013 -0400 @@ -40,10 +40,10 @@ /** Only compile script, do not run it or generate other ScriptObjects */ public final boolean _compile_only; - /** Accumulated callsite flags that will be used when boostrapping script callsites */ + /** Accumulated callsite flags that will be used when bootstrapping script callsites */ public final int _callsite_flags; - /** Genereate line number table in class files */ + /** Generate line number table in class files */ public final boolean _debug_lines; /** Package to which generated class files are added */ diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/runtime/ScriptingFunctions.java --- a/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java Mon Feb 04 14:48:35 2013 -0400 @@ -32,9 +32,15 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; /** * Global functions supported only in scripting mode. @@ -50,6 +56,19 @@ /** Handle to implementation of {@link ScriptingFunctions#quit} - Nashorn extension */ public static final MethodHandle QUIT = findOwnMH("quit", Object.class, Object.class, Object.class); + /** Handle to implementation of {@link ScriptingFunctions#quit} - Nashorn extension */ + public static final MethodHandle EXEC = findOwnMH("exec", Object.class, Object.class, Object.class, Object.class); + + /** Names of special properties used by $EXEC API. */ + public static final String EXEC_NAME = "$EXEC"; + private static final String OUT_NAME = "$OUT"; + private static final String ERR_NAME = "$ERR"; + private static final String EXIT_NAME = "$EXIT"; + + /** Names of special properties used by $ENV API. */ + public static final String ENV_NAME = "$ENV"; + private static final String PWD_NAME = "PWD"; + private ScriptingFunctions() { } @@ -108,6 +127,118 @@ return UNDEFINED; } + /** + * Nashorn extension: exec a string in a separate process. + * + * @param self self reference + * @param string string to execute + * + * @return output string from the request + */ + public static Object exec(final Object self, final Object string, final Object input) throws IOException, InterruptedException { + // Current global is need to fetch additional inputs and for additional results. + final ScriptObject global = Context.getGlobal(); + + // Current ENV property state. + final Object env = global.get(ENV_NAME); + // Make sure ENV is a valid script object. + if (!(env instanceof ScriptObject)) { + typeError("env.not.object"); + } + final ScriptObject envProperties = (ScriptObject)env; + + // Break exec string into tokens. + final StringTokenizer tokenizer = new StringTokenizer(JSType.toString(string)); + final String[] cmdArray = new String[tokenizer.countTokens()]; + for (int i = 0; tokenizer.hasMoreTokens(); i++) { + cmdArray[i] = tokenizer.nextToken(); + } + + // Set up initial process. + final ProcessBuilder processBuilder = new ProcessBuilder(cmdArray); + + // If a working directory is present, use it. + final Object pwd = envProperties.get(PWD_NAME); + if (pwd != UNDEFINED) { + processBuilder.directory(new File(JSType.toString(pwd))); + } + + // Set up ENV variables. + final Map environment = processBuilder.environment(); + environment.clear(); + for (Map.Entry entry : envProperties.entrySet()) { + + environment.put(JSType.toString(entry.getKey()), JSType.toString(entry.getValue())); + } + + // Start the process. + final Process process = processBuilder.start(); + + // If input is present, pass on to process. + try (OutputStream outputStream = process.getOutputStream()) { + if (input != UNDEFINED) { + outputStream.write(JSType.toString(input).getBytes()); + } + } + + // Wait for the process to complete. + final int exit = process.waitFor(); + + // Collect output. + String out; + try (InputStream inputStream = process.getInputStream()) { + final StringBuilder outBuffer = new StringBuilder(); + for (int ch; (ch = inputStream.read()) != -1; ) { + outBuffer.append((char)ch); + } + out = outBuffer.toString(); + } + + // Collect errors. + String err; + try (InputStream errorStream = process.getErrorStream()) { + final StringBuilder errBuffer = new StringBuilder(); + for (int ch; (ch = errorStream.read()) != -1; ) { + errBuffer.append((char)ch); + } + err = errBuffer.toString(); + } + + // Set globals for secondary results. + final boolean isStrict = global.isStrictContext(); + global.set(OUT_NAME, out, isStrict); + global.set(ERR_NAME, err, isStrict); + global.set(EXIT_NAME, exit, isStrict); + + // Return the result from stdout. + return out; + } + + /** + * Return an object containing properties mapping to ENV variables. + * + * @param envProperties object to receive properties + * @param isStrict global's strict state + * + * @return Script object with properties mapping to ENV variables. + */ + public static ScriptObject getENVValues(final ScriptObject envProperties, final boolean isStrict) { + // Retrieve current state of ENV variables. + Map envVars; + try { + envVars = System.getenv(); + } catch(SecurityException ex) { + envVars = new HashMap<>(); + } + + // Map ENV variables. + for (Map.Entry entry : envVars.entrySet()) { + envProperties.set(entry.getKey(), entry.getValue(), isStrict); + } + + return envProperties; + } + private static MethodHandle findOwnMH(final String name, final Class rtype, final Class... types) { return MH.findStatic(MethodHandles.lookup(), ScriptingFunctions.class, name, MH.type(rtype, types)); } diff -r bee7c8a45a04 -r 6f58c28c4faa src/jdk/nashorn/internal/runtime/resources/Messages.properties --- a/src/jdk/nashorn/internal/runtime/resources/Messages.properties Mon Feb 04 16:20:05 2013 +0100 +++ b/src/jdk/nashorn/internal/runtime/resources/Messages.properties Mon Feb 04 14:48:35 2013 -0400 @@ -121,6 +121,7 @@ type.error.no.constructor.matches.args=Can not construct {0} with the passed arguments; they do not match any of its constructor signatures. type.error.no.method.matches.args=Can not invoke method {0} with the passed arguments; they do not match any of its method signatures. type.error.method.not.constructor=Java method {0} can't be used as a constructor. +type.error.env.not.object=$ENV must be an Object. range.error.inappropriate.array.length=inappropriate array length: {0} range.error.invalid.fraction.digits=fractionDigits argument to {0} must be in [0, 20] range.error.invalid.precision=precision argument toPrecision() must be in [1, 21] diff -r bee7c8a45a04 -r 6f58c28c4faa test/script/basic/JDK-8006191.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/script/basic/JDK-8006191.js Mon Feb 04 14:48:35 2013 -0400 @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * JDK-8006191 - `cmd` -> exec("cmd") in script mode + * + * @test + * @option -scripting + * @argument ArgumentFromCommandLine + * @run + */ + +#!/usr/bin/jjs + +$ENV.PWD = "."; +print($ENV.PWD); + +var files = `ls`.trim().split("\n"); +for (var i in files) { + var file = files[i]; + if (file.contains("README")) { + print(file); + } +} + +var result = $EXEC("cat", <