8006191: `cmd` -> exec("cmd") in script mode
authorjlaskey
Mon, 04 Feb 2013 14:48:35 -0400
changeset 16211 41e031a45186
parent 16210 8ad1381b69d0
child 16212 363737d0d757
8006191: `cmd` -> exec("cmd") in script mode Reviewed-by: sundar, lagergren, hannesw Contributed-by: james.laskey@oracle.com
nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java
nashorn/src/jdk/nashorn/internal/objects/Global.java
nashorn/src/jdk/nashorn/internal/parser/Lexer.java
nashorn/src/jdk/nashorn/internal/parser/Parser.java
nashorn/src/jdk/nashorn/internal/parser/TokenType.java
nashorn/src/jdk/nashorn/internal/runtime/Context.java
nashorn/src/jdk/nashorn/internal/runtime/OptionsObject.java
nashorn/src/jdk/nashorn/internal/runtime/ScriptingFunctions.java
nashorn/src/jdk/nashorn/internal/runtime/resources/Messages.properties
nashorn/test/script/basic/JDK-8006191.js
nashorn/test/script/basic/JDK-8006191.js.EXPECTED
--- 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