[ 
https://issues.apache.org/jira/browse/TINKERPOP-3183?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18144058#comment-18144058
 ] 

ASF GitHub Bot commented on TINKERPOP-3183:
-------------------------------------------

kenhuuu commented on code in PR #3773:
URL: https://github.com/apache/tinkerpop/pull/3773#discussion_r4553519033


##########
gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/StatementParser.groovy:
##########
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.console
+
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.codehaus.groovy.control.Phases
+import org.jline.reader.EOFError
+import org.jline.reader.ParsedLine
+import org.jline.reader.Parser
+import org.jline.reader.impl.DefaultParser
+
+import java.util.function.Predicate
+
+/**
+ * Decides when typed, piped or scripted input is a complete statement. The 
line reader's own parser only waits for
+ * more input while a bracket, quote or comment is open, so a line such as 
{@code g.V().} or {@code x = 1 +} would run
+ * at once and fail. This parser also waits when the statement only fails to 
compile because it ends too early.
+ */
+class StatementParser implements Parser {
+    private final Parser delegate
+    private final CompilerConfiguration config
+    private final Predicate<String> isCommand
+
+    /**
+     * @param delegate the line reader's parser
+     * @param config the compiler settings of the engine the statement runs in
+     * @param isCommand whether a name such as {@code /plugin} is a console 
command
+     */
+    StatementParser(final Parser delegate, final CompilerConfiguration config, 
final Predicate<String> isCommand) {
+        this.delegate = delegate
+        this.config = config
+        this.isCommand = isCommand
+    }
+
+    /**
+     * Creates a parser for script files, with the line reader settings 
groovysh uses for typed input.
+     */
+    static StatementParser forScripts(final CompilerConfiguration config) {
+        final DefaultParser lineParser = new DefaultParser(regexCommand: 
/\/?[a-zA-Z!]\S*/, eofOnUnclosedQuote: true,
+                eofOnEscapedNewLine: true)
+        lineParser.blockCommentDelims(new 
DefaultParser.BlockCommentDelims("/*", "*/"))
+                .lineCommentDelims(["//"] as String[])
+                .setEofOnUnclosedBracket(DefaultParser.Bracket.CURLY, 
DefaultParser.Bracket.ROUND, DefaultParser.Bracket.SQUARE)
+        // console commands in scripts are handled before a statement is parsed
+        return new StatementParser(lineParser, config, { false } as 
Predicate<String>)
+    }
+
+    @Override
+    ParsedLine parse(final String line, final int cursor, final ParseContext 
context) {
+        if (context != ParseContext.ACCEPT_LINE) return delegate.parse(line, 
cursor, context)
+
+        final boolean command = isCommandLine(line)
+        try {
+            final ParsedLine parsed = delegate.parse(line, cursor, context)
+            if (!command && continuesOnNextLine(line)) throw new 
StatementContinues()
+            return parsed
+        } catch (EOFError e) {
+            // more input cannot fix a stray closing bracket, and a command 
with an unclosed quote is reported by
+            // the command, so both are accepted as they are
+            if (e instanceof StatementContinues || !(command || 
hasStrayClosingBracket(line))) throw e
+            return delegate.parse(line, cursor, ParseContext.SPLIT_LINE)
+        }
+    }
+
+    /**
+     * Whether the text is an unfinished statement that the next line 
continues.
+     */
+    boolean isUnfinished(final String text) {
+        try {
+            parse(text, text.length(), ParseContext.ACCEPT_LINE)
+            return false
+        } catch (EOFError ignored) {
+            return true
+        }
+    }
+
+    private boolean isCommandLine(final String line) {
+        final String trimmed = line.trim()
+        if (!trimmed.startsWith(":") && !trimmed.startsWith("/")) return false
+        final String name = "/" + trimmed.substring(1).split(/\s+/, 2)[0]
+        return isCommand.test(name)
+    }
+
+    /**
+     * Whether the statement fails to parse only because it ends too early, 
e.g. after a trailing {@code .} or an
+     * operator: it does not parse as it is but does once a name is added 
after it. The position of Groovy's error
+     * cannot be used instead, as {@code repeat(out()).} is reported at the 
call rather than at the end.
+     */
+    boolean continuesOnNextLine(final String statement) {
+        final String code = statement.stripTrailing()
+        // ":" starts no Groovy statement, so an unknown command is a mistake 
rather than an unfinished line
+        if (code.isEmpty() || code.startsWith(":")) return false
+
+        return !parses(code) && parses(code + "\n" + NEXT_LINE)
+    }
+
+    /**
+     * Stands in for whatever the next line holds.
+     */
+    private static final String NEXT_LINE = "_"
+
+    private boolean parses(final String code) {
+        // parse only, so no class is generated or loaded
+        final CompilationUnit unit = new CompilationUnit(config)
+        unit.addSource("Script1.groovy", code)
+        try {
+            unit.compile(Phases.CONVERSION)
+            return true
+        } catch (MultipleCompilationErrorsException ignored) {
+            return false
+        }
+    }
+
+    /**
+     * Whether a closing bracket appears before any matching opening bracket, 
which no further input can fix.
+     * Brackets inside strings count too.
+     */
+    static boolean hasStrayClosingBracket(final String statement) {

Review Comment:
   This method likely has an issue. For example, `foo("))"` is missing the 
call-closing ), but the quoted characters drive the raw counter negative, 
causing the line to execute instead of waiting for continuation.



##########
gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/commands/PluginCommand.groovy:
##########


Review Comment:
   Can we add a failure-path test? If `activate()` throws, shell evaluation 
isn't set back to `true`.



##########
gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy:
##########
@@ -162,85 +144,118 @@ class Console {
         if (activePlugins.size() != mediator.activePlugins().size())
             mediator.writePluginState()
 
-        try {
-            // if the init script contains :x command it will throw an 
ExitNotification so init script execution
-            // needs to appear in the try/catch
-            if (scriptsAndArgs != null && !scriptsAndArgs.isEmpty()) 
executeInShell(scriptsAndArgs)
-
-            // start iterating results to show as output
-            showShellEvaluationOutput(true)
-
-            runner.run()
-        } catch (ExitNotification ignored) {
-            // occurs on exit
-        } catch (Throwable t) {
-            t.printStackTrace()
-        } finally {
-            // shutdown hook defined above will kill any open remotes
-            System.exit(0)
-        }
+        commands = GremlinCommands.create(mediator, engine)
+
+        if (scriptsAndArgs != null && !scriptsAndArgs.isEmpty()) 
executeInShell(scriptsAndArgs, commands)
+
+        showOutput = true
     }
 
-    def showShellEvaluationOutput(final boolean show) {
-        if (show)
-            groovy.setResultHook(handleResultIterate)
-        else
-            groovy.setResultHook(handleResultShowNothing)
+    /**
+     * Runs the shell on the terminal until the session ends and returns its 
exit code.
+     */
+    int start(final Terminal terminal, final Path historyFile = 
Paths.get(ConsoleFs.HISTORY_FILE)) {
+        final GroovyshOptions options = GroovyshOptions.builder()
+                .engine(engine)
+                .terminal(terminal)
+                .showBanner(false)
+                .prompt({ interactive ? 
Colorizer.render(Preferences.inputPromptColor, Preferences.inputPrompt + ' ') : 
"" } as Supplier<String>)
+                .historyFile(historyFile)

Review Comment:
   Could we add a startup test using a path that can't be opened (like a 
directory)? A bad history file should not prevent console use. It seems like we 
don't have testing for this now that we use the JLine history.



##########
gremlin-console/src/test/groovy/org/apache/tinkerpop/gremlin/console/AbstractConsoleSessionTest.groovy:
##########
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.console
+
+import org.jline.jansi.Ansi
+import org.junit.After
+import org.junit.Before
+
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Runs each test against consoles started in this JVM, with its own home 
directory and the preferences reset.
+ */
+abstract class AbstractConsoleSessionTest {
+
+    static final String MODERN_GRAPH = "g = 
traversal().with(TinkerFactory.createModern())"
+    static final String MODERN_GRAPH_RESULT = 
"==>graphtraversalsource[tinkermemorygraph[vertices:6 edges:6], standard]"
+
+    // in-memory in tests (see InMemoryPreferencesFactory)
+    protected static final java.util.prefs.Preferences STORE =
+            
java.util.prefs.Preferences.userRoot().node("/org/apache/tinkerpop/gremlin/console")
+
+    private static boolean preferencesLoaded = false
+
+    protected Path home
+    private String oldHome
+    private boolean oldAnsi
+    private final List<ConsoleSession> sessions = []
+
+    @Before
+    void setUpSession() {
+        // loading Console turns colors on, so load it before they are 
switched off below
+        Class.forName(Console.name)
+        // main() loads the preferences and installs their listeners before it 
creates the console
+        synchronized (AbstractConsoleSessionTest) {
+            if (!preferencesLoaded) {
+                Preferences.expandoMagic()
+                preferencesLoaded = true
+            }
+        }
+        resetPreferences()
+        home = Files.createTempDirectory("console-session-test")
+        oldHome = System.getProperty("user.home")
+        System.setProperty("user.home", home.toString())
+        oldAnsi = Ansi.isEnabled()
+        // as with -C
+        Ansi.enabled = false
+    }
+
+    @After
+    void tearDownSession() {
+        sessions.each { if (it != null) try { it.close() } catch (Throwable 
ignored) { } }

Review Comment:
   Should this be `sessions.reverseEach { ... }` ?



##########
gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy:
##########
@@ -467,6 +504,22 @@ class Console {
         if (!interactive) System.exit(0)
     }
 
+    /**
+     * Runs a console command from a script. Only the Gremlin commands and the 
exit commands are available, as
+     * groovysh's own commands do not exist until the shell starts.
+     */
+    private static void executeCommand(final String line, final CommandGroup 
commands) {
+        final List<String> words = new DefaultParser().parse(line, 
line.length()).words().findAll { !it.isEmpty() }

Review Comment:
   Can we get some more tests in `-i/-e` mode? In particular since 
`DefaultParser` is used here we should have tests to ensure that quoted values, 
spaces, escapes, and multiple commands work? `:plugin unuse` alone won't catch 
issues introduced by DefaultParser.



##########
gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/PluggedIn.groovy:
##########
@@ -54,21 +52,15 @@ class PluggedIn {
     void activate() {
         plugin.getCustomizers("gremlin-groovy").get().each {
             if (it instanceof ImportCustomizer) {
-                if (shell instanceof GremlinGroovysh) {
-                    org.codehaus.groovy.control.customizers.ImportCustomizer 
ic = new org.codehaus.groovy.control.customizers.ImportCustomizer()
-                    ic.addStarImports(it.getClassPackages().collect() { 
it.getName() }.toArray(new String[0]))
-                    ic.addStaticStars(it.getMethodClasses().collect() { 
it.getCanonicalName() }.toArray(new String[0]))
-                    ic.addStaticStars(it.getEnumClasses().collect() { 
it.getCanonicalName() }.toArray(new String[0]))
-                    ((GremlinGroovysh) 
shell).getCompilerConfiguration().addCompilationCustomizers(ic)
-                } else {
-                    it.getClassPackages().collect {Mediator.IMPORT_SPACE + 
it.getName() + Mediator.IMPORT_WILDCARD }.each { shell.execute(it) }
-                    it.getMethodClasses().collect 
{Mediator.IMPORT_STATIC_SPACE + it.getCanonicalName() + 
Mediator.IMPORT_WILDCARD}.each {shell.execute(it)}
-                    it.getEnumClasses().collect {Mediator.IMPORT_STATIC_SPACE 
+ it.getCanonicalName() + Mediator.IMPORT_WILDCARD}.each {shell.execute(it)}
-                }
+                org.codehaus.groovy.control.customizers.ImportCustomizer ic = 
new org.codehaus.groovy.control.customizers.ImportCustomizer()
+                ic.addStarImports(it.getClassPackages().collect() { 
it.getName() }.toArray(new String[0]))
+                ic.addStaticStars(it.getMethodClasses().collect() { 
it.getCanonicalName() }.toArray(new String[0]))
+                ic.addStaticStars(it.getEnumClasses().collect() { 
it.getCanonicalName() }.toArray(new String[0]))
+                engine.getCompilerConfiguration().addCompilationCustomizers(ic)
             } else if (it instanceof ScriptCustomizer) {
-                it.getScripts().collect { it.join(LINE_SEPARATOR) }.each { 
shell.execute(it) }
+                it.getScripts().collect { it.join(LINE_SEPARATOR) }.each { 
engine.execute(it) }
             } else if (it instanceof BindingsCustomizer) {

Review Comment:
   It doesn't seem like there are any test for BindingsCustomizer. Could one be 
added with a test-only plugin?





> Upgrade to Groovy 6
> -------------------
>
>                 Key: TINKERPOP-3183
>                 URL: https://issues.apache.org/jira/browse/TINKERPOP-3183
>             Project: TinkerPop
>          Issue Type: Improvement
>          Components: groovy
>    Affects Versions: 3.7.4
>            Reporter: Stephen Mallette
>            Priority: Major
>
> -Groovy 5 recently released. While Groovy will be removed from Gremlin Server 
> in 4.x it will still exist for Gremlin Console (and perhaps other use cases). 
> Note that for Gremlin Console, there were supposedly significant changes to 
> {{groovysh}} for Groovy 5 because of the upgrade to JLine. It likely will 
> impact Gremlin Console and make the upgrade more complicated.-
> Groovy 6 has now released, which has walked back some of the changes which 
> had complicated the gremlin-console groovy 5 migration. We should aim to jump 
> straight to groovy 6.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to