8006191: `cmd` -> exec("cmd") in script mode
Reviewed-by: sundar, lagergren, hannesw
Contributed-by: james.laskey@oracle.com
--- a/nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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
--- a/nashorn/src/jdk/nashorn/internal/objects/Global.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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() {
--- a/nashorn/src/jdk/nashorn/internal/parser/Lexer.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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);
}
}
--- a/nashorn/src/jdk/nashorn/internal/parser/Parser.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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<Node> 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 :
--- a/nashorn/src/jdk/nashorn/internal/parser/TokenType.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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),
--- a/nashorn/src/jdk/nashorn/internal/runtime/Context.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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();
--- a/nashorn/src/jdk/nashorn/internal/runtime/OptionsObject.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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 */
--- a/nashorn/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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<String, String> environment = processBuilder.environment();
+ environment.clear();
+ for (Map.Entry<Object, Object> 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<String, String> envVars;
+ try {
+ envVars = System.getenv();
+ } catch(SecurityException ex) {
+ envVars = new HashMap<>();
+ }
+
+ // Map ENV variables.
+ for (Map.Entry<String, String> 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));
}
--- a/nashorn/src/jdk/nashorn/internal/runtime/resources/Messages.properties Mon Feb 04 16:20:05 2013 +0100
+++ b/nashorn/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]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/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", <<EOD);
+This is a bunch of stuff
+that I want written out
+including ${$ARG[0]}
+EOD
+print(result);
+print($OUT);
+
+var arg = "-Q";
+`ls ${arg}`;
+print($ERR);
+print($EXIT);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/script/basic/JDK-8006191.js.EXPECTED Mon Feb 04 14:48:35 2013 -0400
@@ -0,0 +1,14 @@
+.
+README
+RELEASE_README
+THIRD_PARTY_README
+This is a bunch of stuff
+that I want written out
+including ArgumentFromCommandLine
+This is a bunch of stuff
+that I want written out
+including ArgumentFromCommandLine
+ls: illegal option -- Q
+usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
+
+1