This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new f1049836f4 GROOVY-8162: Update Groovysh to JLine3 (support /slurp for 
XML, TOML and YAML)
f1049836f4 is described below

commit f1049836f4f3271ca6dc739b2a0b3e01ff489f1d
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Jul 22 21:14:15 2025 +1000

    GROOVY-8162: Update Groovysh to JLine3 (support /slurp for XML, TOML and 
YAML)
---
 subprojects/groovy-groovysh/build.gradle           |   3 +
 .../groovy/groovysh/jline/GroovyCommands.groovy    | 232 ++++++++++++++++++---
 .../groovysh/jline/GroovyConsoleEngine.groovy      |   9 +-
 3 files changed, 209 insertions(+), 35 deletions(-)

diff --git a/subprojects/groovy-groovysh/build.gradle 
b/subprojects/groovy-groovysh/build.gradle
index d3cc2c9f5f..ece13ebdf7 100644
--- a/subprojects/groovy-groovysh/build.gradle
+++ b/subprojects/groovy-groovysh/build.gradle
@@ -31,6 +31,9 @@ dependencies {
     implementation projects.groovyTemplates
     implementation projects.groovyXml
     implementation projects.groovyJson
+    implementation projects.groovyToml
+    implementation projects.groovyNio
+    implementation projects.groovyYaml
     testImplementation projects.groovyTest
     implementation 'net.java.dev.jna:jna:5.17.0'
     implementation "org.jline:jansi:${versions.jline}"
diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy
index 14b4adb240..bea7f5ac7e 100644
--- 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy
@@ -20,6 +20,9 @@ package org.apache.groovy.groovysh.jline
 
 import groovy.console.ui.Console
 import groovy.console.ui.ObjectBrowser
+import groovy.toml.TomlSlurper
+import groovy.xml.XmlParser
+import groovy.yaml.YamlSlurper
 import org.apache.groovy.groovysh.Main
 import org.jline.builtins.Completers
 import org.jline.builtins.Completers.OptDesc
@@ -30,14 +33,22 @@ import org.jline.console.CommandInput
 import org.jline.console.CommandMethods
 import org.jline.console.CommandRegistry
 import org.jline.console.Printer
+import org.jline.console.ScriptEngine
 import org.jline.console.impl.JlineCommandRegistry
+import org.jline.reader.Candidate
 import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
+import org.jline.reader.impl.completer.AggregateCompleter
 import org.jline.reader.impl.completer.ArgumentCompleter
 import org.jline.reader.impl.completer.NullCompleter
 import org.jline.reader.impl.completer.StringsCompleter
 import org.jline.utils.AttributedString
 
 import java.awt.event.ActionListener
+import java.lang.reflect.Method
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
 import java.nio.file.FileSystems
 import java.nio.file.Files
 import java.nio.file.Path
@@ -62,6 +73,7 @@ class GroovyCommands extends JlineCommandRegistry implements 
CommandRegistry {
         '/reset'       : new Tuple4<>(this::reset, this::defCompleter, 
this::defCmdDesc, ['clear the buffer']),
         '/load'        : new Tuple4<>(this::load, this::loadCompleter, 
this::loadCmdDesc, ['load state/a file into the buffer']),
         '/save'        : new Tuple4<>(this::save, this::saveCompleter, 
this::saveCmdDesc, ['save state/the buffer to a file']),
+        '/slurp'       : new Tuple4<>(this::slurpcmd, this::slurpCompleter, 
this::slurpCmdDesc, ['slurp file or string variable context to object']),
         '/types'       : new Tuple4<>(this::typesCommand, 
this::typesCompleter, this::nameDeleteCmdDesc, ['show/delete types']),
         '/methods'     : new Tuple4<>(this::methodsCommand, 
this::methodsCompleter, this::nameDeleteCmdDesc, ['show/delete methods'])
     ]
@@ -222,6 +234,95 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         loadFile(engine, new File(arg), merge)
     }
 
+    void slurpcmd(CommandInput input) {
+        checkArgCount(input, [0, 1, 2, 3, 4])
+        if (maybePrintHelp(input, '/slurp')) return
+        Charset encoding = StandardCharsets.UTF_8
+        String format = null
+        def out = null
+        def args = input.args()
+        int index = 0
+        int optionIndex = optionIdx(args, index)
+        while (optionIndex > -1) {
+            index++
+            def option = args[optionIndex]
+            def arg = null
+            if (option.contains('=')) {
+                arg = option.substring(option.indexOf('=') + 1)
+                option = option.substring(0, option.indexOf('='))
+            } else if (optionIndex + 1 < args.length) {
+                arg = args[optionIndex + 1]
+                index++
+            }
+            if (option in ['-e', '--encoding'] && arg) {
+                encoding = Charset.forName(arg)
+            }
+            if (option in ['-f', '--format'] && arg) {
+                format = arg
+            }
+            optionIndex = optionIdx(args, index)
+        }
+        Object arg = args[index]
+        if (!(arg instanceof String)) {
+            throw new IllegalArgumentException("Invalid parameter type: " + 
arg.getClass().simpleName)
+        }
+        try {
+            Path path = Paths.get(arg)
+            if (Files.exists(path)) {
+                if (!format) {
+                    def ext = path.extension
+                    if (ext.equalsIgnoreCase('json')) {
+                        format = 'JSON'
+                    } else if (ext.equalsIgnoreCase('yaml') || 
ext.equalsIgnoreCase('yml')) {
+                        format = 'YAML'
+                    } else if (ext.equalsIgnoreCase('xml') || 
ext.equalsIgnoreCase('rdf')) {
+                        format = 'XML'
+                    } else if (ext.equalsIgnoreCase('groovy')) {
+                        format = 'GROOVY'
+                    } else if (ext.equalsIgnoreCase('toml')) {
+                        format = 'TOML'
+                    } else if (ext.equalsIgnoreCase('txt') || 
ext.equalsIgnoreCase('text')) {
+                        format = 'TEXT'
+                    }
+                }
+                if (format == 'TEXT') {
+                    out = Files.readAllLines(path, encoding)
+                } else if (format in engine.deserializationFormats) {
+                    byte[] encoded = Files.readAllBytes(path)
+                    out = engine.deserialize(new String(encoded, encoding), 
format)
+                } else if (format == 'XML') {
+                    out = new XmlParser().parse(path.toFile())
+                } else if (format == 'TOML') {
+                    out = new TomlSlurper().parse(path)
+                } else if (format == 'YAML') {
+                    out = new YamlSlurper().parse(path)
+                } else {
+                    out = engine.deserialize(Files.readString(path, encoding), 
'NONE')
+                }
+            } else {
+                if (format == 'TEXT') {
+                    out = arg.readLines()
+                } else if (format in engine.deserializationFormats) {
+                    out = engine.deserialize(arg, format)
+                } else if (format == 'XML') {
+                    out = new XmlParser().parseText(arg)
+                } else if (format == 'TOML') {
+                    out = new TomlSlurper().parseText(arg)
+                } else if (format == 'YAML') {
+                    out = new YamlSlurper().parseText(arg)
+                } else {
+                    out = engine.deserialize(arg, 'NONE')
+                }
+            }
+        } catch (Exception ignore) {
+            println ignore.message
+            ignore.printStackTrace()
+            out = engine.deserialize(arg, format)
+        }
+        engine.put("_", out)
+        printer.println(out)
+    }
+
     static void loadFile(GroovyEngine engine, File file, boolean merge = 
false) {
         if (!file) {
             throw new IllegalArgumentException('File not found: ' + file.path)
@@ -246,7 +347,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         highlighter.highlight(lines.collect{ ' \b' + it 
}.join('\n\n')).toAnsi()
     }
 
-    boolean maybePrintHelp(CommandInput input, String name) {
+    private boolean maybePrintHelp(CommandInput input, String name) {
         if (!input.args()) return false
         String arg = input.args()[0]
         if (arg == '-?' || arg == '--help') {
@@ -256,7 +357,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         return false
     }
 
-    boolean maybeRemoveItem(CommandInput input, String name, Map<String, 
String> aggregate, Closure remove) {
+    private static boolean maybeRemoveItem(CommandInput input, String name, 
Map<String, String> aggregate, Closure remove) {
         if (input.args().length == 2) {
             if (input.args()[0] == '-d' || input.args()[0] == '--delete') {
                 def toRemove = []
@@ -348,18 +449,10 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
     }
 
     def inspect(CommandInput input) {
-        if (!input.xargs()) {
-            return null
-        }
-        if (input.args().length > 2) {
-            throw new IllegalArgumentException('Wrong number of command 
parameters: ' + input.args().length)
-        }
+        checkArgCount(input, [1, 2])
+        if (maybePrintHelp(input, '/inspect')) return
         int idx = optionIdx(input.args())
         String option = idx < 0 ? '--info' : input.args()[idx]
-        if (option == '-?' || option == '--help') {
-            printer.println(helpDesc('/inspect'))
-            return null
-        }
         int id = 0
         if (idx >= 0) {
             id = idx == 0 ? 1 : 0
@@ -471,7 +564,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
 
     private CmdDesc grabCmdDesc(String name) {
         new CmdDesc([
-            new AttributedString("$name [options] 
<group>:<artifact>:<version>"),
+            new AttributedString("$name [OPTIONS] 
<group>:<artifact>:<version>"),
             new AttributedString("$name --list")
         ], [], [
             '-? --help'     : doDescription('Displays command help'),
@@ -480,6 +573,16 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         ])
     }
 
+    private CmdDesc slurpCmdDesc(String name) {
+        new CmdDesc([
+            new AttributedString("$name [OPTIONS] file|variable")
+        ], [], [
+            '-? --help'               : doDescription('Displays command help'),
+            '-e --encoding=ENCODING'  : doDescription('Encoding (default 
UTF-8)'),
+            '-f --format=FORMAT'      : doDescription('Serialization format')
+        ])
+    }
+
     private CmdDesc defCmdDesc(String name) {
         new CmdDesc([
             new AttributedString(name),
@@ -490,7 +593,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
 
     private CmdDesc nameDeleteCmdDesc(String name) {
         new CmdDesc([
-            new AttributedString("$name [options] [name]"),
+            new AttributedString("$name [OPTIONS] [name]"),
         ], [], [
             '-? --help'      : doDescription('Displays command help'),
             '-d --delete'    : doDescription('Delete the named item'),
@@ -499,7 +602,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
 
     private CmdDesc loadCmdDesc(String name) {
         new CmdDesc([
-            new AttributedString("$name [options] [filename]")
+            new AttributedString("$name [OPTIONS] [filename]")
         ], [], [
             '-? --help'     : doDescription('Displays command help'),
             '-m --merge'    : doDescription('Merge into existing buffer')
@@ -508,7 +611,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
 
     private CmdDesc saveCmdDesc(String name) {
         new CmdDesc([
-            new AttributedString("$name [options] [filename]")
+            new AttributedString("$name [OPTIONS] [filename]")
         ], [], [
             '-? --help'       : doDescription('Displays command help'),
             '-o --overwrite'  : doDescription('Overwrite existing file')
@@ -526,7 +629,7 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
             optDescs['-g --gui'] = doDescription('Launch object browser')
         }
         new CmdDesc([
-            new AttributedString("$name [OPTION] OBJECT"),
+            new AttributedString("$name [OPTIONS] OBJECT"),
         ], [], optDescs)
     }
 
@@ -545,15 +648,8 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         [new AttributedString(description)]
     }
 
-    private int optionIdx(String[] args) {
-        int out = 0
-        for (String a : args) {
-            if (a.startsWith('-')) {
-                return out
-            }
-            out++
-        }
-        return -1
+    private static int optionIdx(String[] args, idx = 0) {
+        args.findIndexOf(idx) { it.startsWith('-') }
     }
 
     private List<String> variables() {
@@ -564,8 +660,8 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         def tuple = commands[command]
         List<OptDesc> out = []
         for (Map.Entry<String, List<AttributedString>> entry : 
tuple.v3(command).optsDesc.entrySet() ) {
-            String[] option = entry.getKey().split(/\s+/)
-            String desc = entry.value.get(0).toString()
+            String[] option = entry.key.split(/\s+/)
+            String desc = entry.value[0].toString()
             if (option.length == 2) {
                 out.add(new OptDesc(option[0], option[1], desc))
             } else if (option[0].charAt(1) == '-') {
@@ -595,6 +691,19 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
         [new ArgumentCompleter(NullCompleter.INSTANCE, new OptionCompleter(new 
Completers.FilesCompleter(workDir), this::compileOptDescs, 1))]
     }
 
+    private List<Completer> slurpCompleter(String command) {
+        for (OptDesc o in compileOptDescs(command)) {
+            if (o.shortOption()?.equals('-f')) {
+                o.valueCompleter = new StringsCompleter('JSON', 'GROOVY', 
'NONE', 'TEXT', 'YAML', 'TOML', 'XML')
+                break
+            }
+        }
+        [new ArgumentCompleter(
+            NullCompleter.INSTANCE,
+            new OptionCompleter(Arrays.asList(new AggregateCompleter(new 
Completers.FilesCompleter(workDir),
+                new VariableReferenceCompleter(engine)), 
NullCompleter.INSTANCE), this::compileOptDescs, 1))]
+    }
+
     private List<Completer> importsCompleter(String command) {
         [new ArgumentCompleter(NullCompleter.INSTANCE,
             new OptionCompleter([new StringsCompleter((Supplier)() -> 
engine.imports.keySet()), NullCompleter.INSTANCE], this::compileOptDescs, 1))]
@@ -635,4 +744,71 @@ class GroovyCommands extends JlineCommandRegistry 
implements CommandRegistry {
                 new OptionCompleter(NullCompleter.INSTANCE,
                         this::compileOptDescs, 1)).tap{strict = false }]
     }
+
+    // Temporarily mimicking code from ConsoleEngineImpl (remove if a viable 
extension point to access it emerges)
+    private static class VariableReferenceCompleter implements Completer {
+        private final ScriptEngine engine
+
+        VariableReferenceCompleter(ScriptEngine engine) {
+            this.engine = engine
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        void complete(LineReader reader, ParsedLine commandLine, 
List<Candidate> candidates) {
+            assert commandLine != null
+            assert candidates != null
+            String word = commandLine.word()
+            try {
+                if (!word.contains('.') && !word.contains('}')) {
+                    for (String v : engine.find().keySet()) {
+                        String c = '${' + v + '}'
+                        candidates.add(new 
Candidate(AttributedString.stripAnsi(c), c, null, null, null, null, false))
+                    }
+                } else if (word.startsWith('${') && word.contains('}') && 
word.contains('.')) {
+                    String var = word.substring(2, word.indexOf('}'))
+                    if (engine.hasVariable(var)) {
+                        String curBuf = word.substring(0, 
word.lastIndexOf('.'))
+                        String objStatement = curBuf.replace('${', ' 
').replace('}', '')
+                        Object obj = curBuf.contains('.') ? 
engine.execute(objStatement) : engine.get(var)
+                        Map<?, ?> map = obj instanceof Map ? (Map<?, ?>) obj : 
null
+                        Set<String> identifiers = new HashSet<>()
+                        if (map != null
+                            && !map.isEmpty()
+                            && map.keySet().iterator().next() instanceof 
String) {
+                            identifiers = (Set<String>) map.keySet()
+                        } else if (map == null && obj != null) {
+                            identifiers = 
getClassMethodIdentifiers(obj.getClass())
+                        }
+                        for (String key : identifiers) {
+                            candidates.add(new 
Candidate(AttributedString.stripAnsi(curBuf + "." + key), key, null, null, 
null, null, false))
+                        }
+                    }
+                }
+            } catch (Exception ignore) {
+            }
+        }
+
+        private static Set<String> getClassMethodIdentifiers(Class<?> clazz) {
+            Set<String> out = new HashSet<>()
+            do {
+                for (Method m : clazz.getMethods()) {
+                    if (!m.isSynthetic() && m.getParameterCount() == 0) {
+                        String name = m.getName()
+                        if (name.matches("get[A-Z].*")) {
+                            out.add(convertGetMethod2identifier(name))
+                        }
+                    }
+                }
+                clazz = clazz.getSuperclass()
+            } while (clazz != null)
+            return out
+        }
+
+        private static String convertGetMethod2identifier(String name) {
+            char[] c = name.substring(3).toCharArray()
+            c[0] = Character.toLowerCase(c[0])
+            return new String(c)
+        }
+    }
 }
diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy
index 522c71579a..df37e643c7 100644
--- 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy
@@ -30,14 +30,9 @@ class GroovyConsoleEngine extends ConsoleEngineImpl {
     private final Printer printer
 
     GroovyConsoleEngine(ScriptEngine engine, Printer printer, Supplier<Path> 
workDir, ConfigurationPath configPath) {
-        super(engine, printer, workDir, configPath)
+        super(Command.values().toSet() - Command.SLURP, engine, printer, 
workDir, configPath)
         this.printer = printer
-        commandNames().each { name ->
-            if (!name.equals('slurp')) {
-                rename(Command."${name.toUpperCase()}", "/$name")
-            }
-        }
-        alias('/slurp', 'slurp')
+        commandNames().each{ name -> rename(Command."${name.toUpperCase()}", 
"/$name") }
     }
 
     void println(Map<String, Object> options, Object object) {

Reply via email to