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 4cc59b2ba1 GROOVY-8162: Update Groovysh to JLine3 (support globbing 
for /cat, /less, /ls)
4cc59b2ba1 is described below

commit 4cc59b2ba1531f8375ff5c6fe116a79be3116b26
Author: Paul King <[email protected]>
AuthorDate: Tue Aug 19 12:15:45 2025 +1000

    GROOVY-8162: Update Groovysh to JLine3 (support globbing for /cat, /less, 
/ls)
---
 .../groovy/org/apache/groovy/groovysh/Main.groovy  |  32 +-
 .../groovy/groovysh/jline/GroovyBuiltins.groovy    |   4 +-
 .../groovy/groovysh/jline/GroovyPosixCommands.java | 867 +++++++++++++++++++++
 .../groovy/groovysh/jline/SystemRegistryImpl.java  |  16 +-
 4 files changed, 910 insertions(+), 9 deletions(-)

diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
index 9852c1cc48..3ca95a99fd 100644
--- 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
@@ -24,6 +24,7 @@ import org.apache.groovy.groovysh.jline.GroovyBuiltins
 import org.apache.groovy.groovysh.jline.GroovyCommands
 import org.apache.groovy.groovysh.jline.GroovyConsoleEngine
 import org.apache.groovy.groovysh.jline.GroovyEngine
+import org.apache.groovy.groovysh.jline.GroovyPosixCommands
 import org.apache.groovy.groovysh.jline.GroovyPosixContext
 import org.apache.groovy.groovysh.jline.GroovySystemRegistry
 import org.apache.groovy.groovysh.util.DocFinder
@@ -81,7 +82,7 @@ import static org.jline.jansi.AnsiRenderer.render
 class Main {
     private static final MessageSource messages = new MessageSource(Main)
     public static final String INTERPRETER_MODE_PREFERENCE_KEY = 
'interpreterMode'
-    private static POSIX_FILE_CMDS = ['/tail', '/ls', '/head', '/grep', '/wc', 
'/sort', '/cat']
+    private static POSIX_FILE_CMDS = ['/tail', '/head', '/wc', '/sort']
 
     @SuppressWarnings("resource")
     protected static class ExtraConsoleCommands extends JlineCommandRegistry 
implements CommandRegistry {
@@ -110,6 +111,9 @@ class Main {
                 '/cd'   : new CommandMethods((Function) this::cd, 
this::optDirCompleter),
                 '/date' : new CommandMethods((Function) this::date, 
this::defaultCompleter),
                 '/echo' : new CommandMethods((Function) this::echo, 
this::defaultCompleter),
+                '/ls'   : new CommandMethods((Function) this::ls, 
this::optFileCompleter),
+                '/grep' : new CommandMethods((Function) this::grepcmd, 
this::optFileCompleter),
+                '/cat'  : new CommandMethods((Function) this::cat, 
this::optFileCompleter),
                 "/!"    : new CommandMethods((Function) this::shell, 
this::defaultCompleter)
             ]
             POSIX_FILE_CMDS.each { String cmd ->
@@ -170,6 +174,30 @@ class Main {
             }
         }
 
+        private void ls(CommandInput input) {
+            try {
+                GroovyPosixCommands.ls(posix.context, ['/ls', *input.args()] 
as String[])
+            } catch (Exception e) {
+                saveException(e)
+            }
+        }
+
+        private void grepcmd(CommandInput input) {
+            try {
+                GroovyPosixCommands.grep(posix.context, ['/grep', 
*input.args()] as String[])
+            } catch (Exception e) {
+                saveException(e)
+            }
+        }
+
+        private void cat(CommandInput input) {
+            try {
+                GroovyPosixCommands.cat(posix.context, ['/cat', *input.args()] 
as String[])
+            } catch (Exception e) {
+                saveException(e)
+            }
+        }
+
         private void date(CommandInput input) {
             posix(adjustUsage('date', '/date'), input)
         }
@@ -393,7 +421,7 @@ class Main {
                 if (!OSUtils.IS_WINDOWS) {
                     setSpecificHighlighter("/!", 
SyntaxHighlighter.build(jnanorc, "SH-REPL"))
                 }
-                addFileHighlight('/nano', '/less', '/slurp', '/load', '/save', 
*POSIX_FILE_CMDS, '/cd')
+                addFileHighlight('/nano', '/less', '/slurp', '/load', '/save', 
*POSIX_FILE_CMDS, '/cd', '/ls', '/cat', '/grep')
                 addFileHighlight('/classloader', null, ['-a', '--add'])
                 addExternalHighlighterRefresh(printer::refresh)
                 addExternalHighlighterRefresh(scriptEngine::refresh)
diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyBuiltins.groovy
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyBuiltins.groovy
index f75659c0b0..f543957c8d 100644
--- 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyBuiltins.groovy
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyBuiltins.groovy
@@ -62,7 +62,7 @@ class GroovyBuiltins extends Builtins {
         Options opt = Options.compile(*Less.usage()*.replaceAll('less ', 
'/less ')).parse(input.args())
         try {
             if (opt.isSet("help")) {
-                throw new Options.HelpException(opt.usage());
+                throw new Options.HelpException(opt.usage())
             }
             boolean usingBuffer = opt.args().size() == 0
             if (usingBuffer) {
@@ -70,7 +70,7 @@ class GroovyBuiltins extends Builtins {
                 temp.text = engine.buffer
                 input = new CommandInput(input.command(), [*input.args(), 
temp.absolutePath] as String[], input.terminal(), input.in(), input.out(), 
input.err())
             }
-            Commands.less(input.terminal(), input.in(), input.out(), 
input.err(), workDir.get(), input.xargs(), configPath)
+            GroovyPosixCommands.less(new GroovyPosixContext(input.in(), new 
PrintStream(input.out()), new PrintStream(input.err()), workDir.get(), 
input.terminal(), engine::get), ['/less', *input.args()] as String[])
         } catch (Exception e) {
             saveException(e)
         }
diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
new file mode 100644
index 0000000000..60c8a738e6
--- /dev/null
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
@@ -0,0 +1,867 @@
+/*
+ *  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.groovy.groovysh.jline;
+
+import org.jline.builtins.Less;
+import org.jline.builtins.Options;
+import org.jline.builtins.PosixCommands;
+import org.jline.builtins.Source;
+import org.jline.terminal.Terminal;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.InputStreamReader;
+import org.jline.utils.OSUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.IntBinaryOperator;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+// The following file is expected to be deleted if/when the following issues 
have been merged in JLine3:
+// https://github.com/jline/jline3/pull/1400
+// https://github.com/jline/jline3/pull/1398
+// https://github.com/jline/jline3/pull/1390
+@Deprecated
+public class GroovyPosixCommands extends PosixCommands {
+
+    public static void cat(Context context, String[] argv) throws Exception {
+        final String[] usage = {
+            "/cat - concatenate and print FILES",
+            "Usage: /cat [OPTIONS] [FILES]",
+            "  -? --help                show help",
+            "  -n                       number the output lines, starting at 1"
+        };
+        Options opt = parseOptions(context, usage, argv);
+
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            args = Collections.singletonList("-");
+        }
+        List<InputStream> expanded = new ArrayList<>();
+        for (String arg : args) {
+            if ("-".equals(arg)) {
+                expanded.add(context.in());
+            } else {
+                expanded.addAll(maybeExpandGlob(context, arg).stream()
+                    .map(p -> {
+                        try {
+                            return p.toUri().toURL().openStream();
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                    })
+                    .collect(Collectors.toList()));
+            }
+        }
+        for (InputStream is : expanded) {
+            cat(context, new BufferedReader(new InputStreamReader(is)), 
opt.isSet("n"));
+        }
+    }
+
+    private static void cat(Context context, BufferedReader reader, boolean 
numbered) throws IOException {
+        String line;
+        int lineno = 1;
+        try {
+            while ((line = reader.readLine()) != null) {
+                if (numbered) {
+                    context.out().printf("%6d\t%s%n", lineno++, line);
+                } else {
+                    context.out().println(line);
+                }
+            }
+        } finally {
+            reader.close();
+        }
+    }
+
+    public static void ls(Context context, String[] argv) throws Exception {
+        final String[] usage = {
+            "/ls - list files",
+            "Usage: /ls [OPTIONS] [PATTERNS...]",
+            "  -? --help                show help",
+            "  -1                       list one entry per line",
+            "  -C                       multi-column output",
+            "     --color=WHEN          colorize the output, may be `always', 
`never' or `auto'",
+            "  -a                       list entries starting with .",
+            "  -F                       append file type indicators",
+            "  -m                       comma separated",
+            "  -l                       long listing",
+            "  -S                       sort by size",
+            "  -f                       output is not sorted",
+            "  -r                       reverse sort order",
+            "  -t                       sort by modification time",
+            "  -x                       sort horizontally",
+            "  -L                       list referenced file for links",
+            "  -h                       print sizes in human readable form"
+        };
+        Options opt = parseOptions(context, usage, argv);
+
+        Map<String, String> colorMap = getLsColorMap(context);
+
+        String color = opt.isSet("color") ? opt.get("color") : "auto";
+        boolean colored;
+        switch (color) {
+            case "always":
+            case "yes":
+            case "force":
+                colored = true;
+                break;
+            case "never":
+            case "no":
+            case "none":
+                colored = false;
+                break;
+            case "auto":
+            case "tty":
+            case "if-tty":
+                colored = context.isTty();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid argument '" + 
color + "' for '--color'");
+        }
+        Map<String, String> colors =
+            colored ? (colorMap != null ? colorMap : 
getLsColorMap(DEFAULT_LS_COLORS)) : Collections.emptyMap();
+
+        class PathEntry implements Comparable<PathEntry> {
+            final Path abs;
+            final Path path;
+            final Map<String, Object> attributes;
+
+            public PathEntry(Path abs, Path root) {
+                this.abs = abs;
+                try {
+                    this.path = Files.isSameFile(abs, root)
+                        ? Paths.get(".")
+                        : abs.startsWith(root) ? root.relativize(abs) : abs;
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+                this.attributes = readAttributes(abs);
+            }
+
+            @Override
+            public int compareTo(PathEntry o) {
+                int c = doCompare(o);
+                return opt.isSet("r") ? -c : c;
+            }
+
+            private int doCompare(PathEntry o) {
+                if (opt.isSet("f")) {
+                    return -1;
+                }
+                if (opt.isSet("S")) {
+                    long s0 = attributes.get("size") != null ? ((Number) 
attributes.get("size")).longValue() : 0L;
+                    long s1 = o.attributes.get("size") != null ? ((Number) 
o.attributes.get("size")).longValue() : 0L;
+                    return s0 > s1 ? -1 : s0 < s1 ? 1 : 
path.toString().compareTo(o.path.toString());
+                }
+                if (opt.isSet("t")) {
+                    long t0 = attributes.get("lastModifiedTime") != null
+                        ? ((FileTime) 
attributes.get("lastModifiedTime")).toMillis()
+                        : 0L;
+                    long t1 = o.attributes.get("lastModifiedTime") != null
+                        ? ((FileTime) 
o.attributes.get("lastModifiedTime")).toMillis()
+                        : 0L;
+                    return t0 > t1 ? -1 : t0 < t1 ? 1 : 
path.toString().compareTo(o.path.toString());
+                }
+                return path.toString().compareTo(o.path.toString());
+            }
+
+            boolean isNotDirectory() {
+                return is("isRegularFile") || is("isSymbolicLink") || 
is("isOther");
+            }
+
+            boolean isDirectory() {
+                return is("isDirectory");
+            }
+
+            private boolean is(String attr) {
+                Object d = attributes.get(attr);
+                return d instanceof Boolean && (Boolean) d;
+            }
+
+            String display() {
+                String type;
+                String suffix;
+                String link = "";
+                if (is("isSymbolicLink")) {
+                    type = "sl";
+                    suffix = "@";
+                    try {
+                        Path l = Files.readSymbolicLink(abs);
+                        link = " -> " + l.toString();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                } else if (is("isDirectory")) {
+                    type = "dr";
+                    suffix = "/";
+                } else if (is("isExecutable")) {
+                    type = "ex";
+                    suffix = "*";
+                } else if (is("isOther")) {
+                    type = "ot";
+                    suffix = "";
+                } else {
+                    type = "";
+                    suffix = "";
+                }
+                boolean addSuffix = opt.isSet("F");
+                return applyStyle(path.toString(), colors, type) + (addSuffix 
? suffix : "") + link;
+            }
+
+            String longDisplay() {
+                String username;
+                if (attributes.containsKey("owner")) {
+                    username = Objects.toString(attributes.get("owner"), null);
+                } else {
+                    username = "owner";
+                }
+                if (username.length() > 8) {
+                    username = username.substring(0, 8);
+                } else {
+                    for (int i = username.length(); i < 8; i++) {
+                        username = username + " ";
+                    }
+                }
+                String group;
+                if (attributes.containsKey("group")) {
+                    group = Objects.toString(attributes.get("group"), null);
+                } else {
+                    group = "group";
+                }
+                if (group.length() > 8) {
+                    group = group.substring(0, 8);
+                } else {
+                    for (int i = group.length(); i < 8; i++) {
+                        group = group + " ";
+                    }
+                }
+                Number length = (Number) attributes.get("size");
+                if (length == null) {
+                    length = 0L;
+                }
+                String lengthString;
+                if (opt.isSet("h")) {
+                    double l = length.longValue();
+                    String unit = "B";
+                    if (l >= 1000) {
+                        l /= 1024;
+                        unit = "K";
+                        if (l >= 1000) {
+                            l /= 1024;
+                            unit = "M";
+                            if (l >= 1000) {
+                                l /= 1024;
+                                unit = "T";
+                            }
+                        }
+                    }
+                    if (l < 10 && length.longValue() > 1000) {
+                        lengthString = String.format("%.1f", l) + unit;
+                    } else {
+                        lengthString = String.format("%3.0f", l) + unit;
+                    }
+                } else {
+                    lengthString = String.format("%1$8s", length);
+                }
+                @SuppressWarnings("unchecked")
+                Set<PosixFilePermission> perms = (Set<PosixFilePermission>) 
attributes.get("permissions");
+                if (perms == null) {
+                    perms = EnumSet.noneOf(PosixFilePermission.class);
+                }
+                // TODO: all fields should be padded to align
+                return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" 
: (is("isOther") ? "o" : "-")))
+                    + PosixFilePermissions.toString(perms) + " "
+                    + String.format(
+                    "%3s",
+                    (attributes.containsKey("nlink")
+                        ? attributes.get("nlink").toString()
+                        : "1"))
+                    + " " + username + " " + group + " " + lengthString + " "
+                    + toString((FileTime) attributes.get("lastModifiedTime"))
+                    + " " + display();
+            }
+
+            protected String toString(FileTime time) {
+                long millis = (time != null) ? time.toMillis() : -1L;
+                if (millis < 0L) {
+                    return "------------";
+                }
+                ZonedDateTime dt = 
Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
+                // Less than six months
+                if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 
60L * 1000L) {
+                    return DateTimeFormatter.ofPattern("MMM ppd 
HH:mm").format(dt);
+                }
+                // Older than six months
+                else {
+                    return DateTimeFormatter.ofPattern("MMM ppd  
yyyy").format(dt);
+                }
+            }
+
+            protected Map<String, Object> readAttributes(Path path) {
+                Map<String, Object> attrs = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+                for (String view : 
path.getFileSystem().supportedFileAttributeViews()) {
+                    try {
+                        Map<String, Object> ta =
+                            Files.readAttributes(path, view + ":*", 
getLinkOptions(opt.isSet("L")));
+                        ta.forEach(attrs::putIfAbsent);
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                attrs.computeIfAbsent("isExecutable", s -> 
Files.isExecutable(path));
+                attrs.computeIfAbsent("permissions", s -> 
getPermissionsFromFile(path));
+                return attrs;
+            }
+        }
+
+        Path currentDir = context.currentDir();
+        // Listing
+        List<Path> expanded = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            expanded.add(currentDir);
+        } else {
+            opt.args().forEach(s -> {
+                expanded.addAll(maybeExpandGlob(context, s));
+            });
+        }
+        boolean listAll = opt.isSet("a");
+        Predicate<Path> filter = p -> listAll
+            || p.getFileName().toString().equals(".")
+            || p.getFileName().toString().equals("..")
+            || !p.getFileName().toString().startsWith(".");
+        List<PathEntry> all = expanded.stream()
+            .filter(filter)
+            .map(p -> new PathEntry(p, currentDir))
+            .sorted()
+            .collect(Collectors.toList());
+        // Print files first
+        List<PathEntry> files = 
all.stream().filter(PathEntry::isNotDirectory).collect(Collectors.toList());
+        PrintStream out = context.out();
+        Consumer<Stream<PathEntry>> display = s -> {
+            boolean optLine = opt.isSet("1");
+            boolean optComma = opt.isSet("m");
+            boolean optLong = opt.isSet("l");
+            boolean optCol = opt.isSet("C");
+            if (!optLine && !optComma && !optLong && !optCol) {
+                if (context.isTty()) {
+                    optCol = true;
+                } else {
+                    optLine = true;
+                }
+            }
+            // One entry per line
+            if (optLine) {
+                s.map(PathEntry::display).forEach(out::println);
+            }
+            // Comma separated list
+            else if (optComma) {
+                
out.println(s.map(PathEntry::display).collect(Collectors.joining(", ")));
+            }
+            // Long listing
+            else if (optLong) {
+                s.map(PathEntry::longDisplay).forEach(out::println);
+            }
+            // Column listing
+            else if (optCol) {
+                toColumn(context, out, s.map(PathEntry::display), 
opt.isSet("x"));
+            }
+        };
+        boolean space = false;
+        if (!files.isEmpty()) {
+            display.accept(files.stream());
+            space = true;
+        }
+        // Print directories
+        List<PathEntry> directories =
+            
all.stream().filter(PathEntry::isDirectory).collect(Collectors.toList());
+        for (PathEntry entry : directories) {
+            if (space) {
+                out.println();
+            }
+            space = true;
+            Path path = currentDir.resolve(entry.path);
+            if (expanded.size() > 1) {
+                out.println(currentDir.relativize(path).toString() + ":");
+            }
+            try (Stream<Path> pathStream = Files.list(path)) {
+                display.accept(Stream.concat(Stream.of(".", 
"..").map(path::resolve), pathStream)
+                    .filter(filter)
+                    .map(p -> new PathEntry(p, path))
+                    .sorted());
+            }
+        }
+    }
+
+    private static void toColumn(Context context, PrintStream out, 
Stream<String> ansi, boolean horizontal) {
+        Terminal terminal = context.terminal();
+        int width = context.isTty() ? terminal.getWidth() : 80;
+        List<AttributedString> strings = 
ansi.map(AttributedString::fromAnsi).collect(Collectors.toList());
+        if (!strings.isEmpty()) {
+            int max = strings.stream()
+                .mapToInt(AttributedString::columnLength)
+                .max()
+                .getAsInt();
+            int c = Math.max(1, width / max);
+            while (c > 1 && c * max + (c - 1) >= width) {
+                c--;
+            }
+            int columns = c;
+            int lines = (strings.size() + columns - 1) / columns;
+            IntBinaryOperator index;
+            if (horizontal) {
+                index = (i, j) -> i * columns + j;
+            } else {
+                index = (i, j) -> j * lines + i;
+            }
+            AttributedStringBuilder sb = new AttributedStringBuilder();
+            for (int i = 0; i < lines; i++) {
+                for (int j = 0; j < columns; j++) {
+                    int idx = index.applyAsInt(i, j);
+                    if (idx < strings.size()) {
+                        AttributedString str = strings.get(idx);
+                        boolean hasRightItem = j < columns - 1 && 
index.applyAsInt(i, j + 1) < strings.size();
+                        sb.append(str);
+                        if (hasRightItem) {
+                            for (int k = 0; k <= max - str.length(); k++) {
+                                sb.append(' ');
+                            }
+                        }
+                    }
+                }
+                sb.append('\n');
+            }
+            out.print(sb.toAnsi(terminal));
+        }
+    }
+
+    public static void grep(Context context, String[] argv) throws Exception {
+        final String[] usage = {
+            "grep -  search for PATTERN in each FILE or standard input.",
+            "Usage: grep [OPTIONS] PATTERN [FILES]",
+            "  -? --help                Show help",
+            "  -i --ignore-case         Ignore case distinctions",
+            "  -n --line-number         Prefix each line with line number 
within its input file",
+            "  -q --quiet, --silent     Suppress all normal output",
+            "  -v --invert-match        Select non-matching lines",
+            "  -w --word-regexp         Select only whole words",
+            "  -x --line-regexp         Select only whole lines",
+            "  -c --count               Only print a count of matching lines 
per file",
+            "     --color=WHEN          Use markers to distinguish the 
matching string, may be `always', `never' or `auto'",
+            "  -B --before-context=NUM  Print NUM lines of leading context 
before matching lines",
+            "  -A --after-context=NUM   Print NUM lines of trailing context 
after matching lines",
+            "  -C --context=NUM         Print NUM lines of output context",
+            "     --pad-lines           Pad line numbers"
+        };
+        Options opt = parseOptions(context, usage, argv);
+
+        Map<String, String> colorMap = getColorMap(context, "GREP", 
DEFAULT_GREP_COLORS);
+
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            throw new IllegalArgumentException("no pattern supplied");
+        }
+
+        String regex = args.remove(0);
+        String regexp = regex;
+        if (opt.isSet("word-regexp")) {
+            regexp = "\\b" + regexp + "\\b";
+        }
+        if (opt.isSet("line-regexp")) {
+            regexp = "^" + regexp + "$";
+        } else {
+            regexp = ".*" + regexp + ".*";
+        }
+        Pattern p;
+        Pattern p2;
+        if (opt.isSet("ignore-case")) {
+            p = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE);
+            p2 = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+        } else {
+            p = Pattern.compile(regexp);
+            p2 = Pattern.compile(regex);
+        }
+        int after = opt.isSet("after-context") ? 
opt.getNumber("after-context") : -1;
+        int before = opt.isSet("before-context") ? 
opt.getNumber("before-context") : -1;
+        int contextLines = opt.isSet("context") ? opt.getNumber("context") : 0;
+        String lineFmt = opt.isSet("pad-lines") ? "%6d" : "%d";
+        if (after < 0) {
+            after = contextLines;
+        }
+        if (before < 0) {
+            before = contextLines;
+        }
+        boolean count = opt.isSet("count");
+        boolean quiet = opt.isSet("quiet");
+        boolean invert = opt.isSet("invert-match");
+        boolean lineNumber = opt.isSet("line-number");
+        String color = opt.isSet("color") ? opt.get("color") : "auto";
+        boolean colored;
+        switch (color) {
+            case "always":
+            case "yes":
+            case "force":
+                colored = true;
+                break;
+            case "never":
+            case "no":
+            case "none":
+                colored = false;
+                break;
+            case "auto":
+            case "tty":
+            case "if-tty":
+                colored = context.isTty();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid argument '" + 
color + "' for '--color'");
+        }
+        Map<String, String> colors =
+            colored ? (colorMap != null ? colorMap : 
getColorMap(DEFAULT_GREP_COLORS)) : Collections.emptyMap();
+
+        if (args.isEmpty()) {
+            args.add("-");
+        }
+        List<GrepSource> sources = new ArrayList<>();
+        for (String arg : args) {
+            if ("-".equals(arg)) {
+                sources.add(new GrepSource(context.in(), "(standard input)"));
+            } else {
+                Path path = context.currentDir().resolve(arg);
+                sources.add(new GrepSource(path, arg));
+            }
+        }
+        boolean match = false;
+        for (GrepSource src : sources) {
+            List<String> lines = new ArrayList<>();
+            boolean firstPrint = true;
+            int nb = 0;
+            try (InputStream is = src.getInputStream()) {
+                try (BufferedReader r = new BufferedReader(new 
InputStreamReader(is))) {
+                    String line;
+                    int lineno = 1;
+                    int lineMatch = 0;
+                    while ((line = r.readLine()) != null) {
+                        boolean matches = p.matcher(line).matches();
+                        if (invert) {
+                            matches = !matches;
+                        }
+                        AttributedStringBuilder sbl = new 
AttributedStringBuilder();
+                        if (matches) {
+                            nb++;
+                            if (!count && !quiet) {
+                                if (sources.size() > 1) {
+                                    if (colored) {
+                                        applyStyle(sbl, colors, "fn");
+                                    }
+                                    sbl.append(src.getName());
+                                    if (colored) {
+                                        applyStyle(sbl, colors, "se");
+                                    }
+                                    sbl.append(":");
+                                }
+                                if (lineNumber) {
+                                    if (colored) {
+                                        applyStyle(sbl, colors, "ln");
+                                    }
+                                    sbl.append(String.format(lineFmt, lineno));
+                                    if (colored) {
+                                        applyStyle(sbl, colors, "se");
+                                    }
+                                    sbl.append(":");
+                                }
+                                if (colored) {
+                                    Matcher matcher2 = p2.matcher(line);
+                                    int cur = 0;
+                                    while (matcher2.find()) {
+                                        applyStyle(sbl, colors, "se");
+                                        sbl.append(line, cur, 
matcher2.start());
+                                        applyStyle(sbl, colors, "ms");
+                                        sbl.append(line, matcher2.start(), 
matcher2.end());
+                                        applyStyle(sbl, colors, "se");
+                                        cur = matcher2.end();
+                                    }
+                                    sbl.append(line, cur, line.length());
+                                } else {
+                                    sbl.append(line);
+                                }
+                                while (lineMatch > after && !lines.isEmpty()) {
+                                    context.out().println(lines.remove(0));
+                                    lineMatch--;
+                                }
+                                lineMatch = Math.min(before, lines.size()) + 
after + 1;
+                            }
+                        } else if (lineMatch > 0) {
+                            context.out().println(lines.remove(0));
+                            lineMatch--;
+                            if (sources.size() > 1) {
+                                if (colored) {
+                                    applyStyle(sbl, colors, "fn");
+                                }
+                                sbl.append(src.getName());
+                                if (colored) {
+                                    applyStyle(sbl, colors, "se");
+                                }
+                                sbl.append("-");
+                            }
+                            if (lineNumber) {
+                                if (colored) {
+                                    applyStyle(sbl, colors, "ln");
+                                }
+                                sbl.append(String.format(lineFmt, lineno));
+                                if (colored) {
+                                    applyStyle(sbl, colors, "se");
+                                }
+                                sbl.append("-");
+                            }
+                            if (colored) {
+                                applyStyle(sbl, colors, "se");
+                            }
+                            sbl.append(line);
+                        } else {
+                            if (sources.size() > 1) {
+                                if (colored) {
+                                    applyStyle(sbl, colors, "fn");
+                                }
+                                sbl.append(src.getName());
+                                if (colored) {
+                                    applyStyle(sbl, colors, "se");
+                                }
+                                sbl.append("-");
+                            }
+                            if (lineNumber) {
+                                if (colored) {
+                                    applyStyle(sbl, colors, "ln");
+                                }
+                                sbl.append(String.format(lineFmt, lineno));
+                                if (colored) {
+                                    applyStyle(sbl, colors, "se");
+                                }
+                                sbl.append("-");
+                            }
+                            if (colored) {
+                                applyStyle(sbl, colors, "se");
+                            }
+                            sbl.append(line);
+                            while (lines.size() > before) {
+                                lines.remove(0);
+                            }
+                            lineMatch = 0;
+                        }
+                        lines.add(sbl.toAnsi(context.terminal()));
+                        while (lineMatch == 0 && lines.size() > before) {
+                            lines.remove(0);
+                        }
+                        lineno++;
+                    }
+                    if (!count && lineMatch > 0) {
+                        if (!firstPrint && before + after > 0) {
+                            AttributedStringBuilder sbl2 = new 
AttributedStringBuilder();
+                            if (colored) {
+                                applyStyle(sbl2, colors, "se");
+                            }
+                            sbl2.append("--");
+                            
context.out().println(sbl2.toAnsi(context.terminal()));
+                        } else {
+                            firstPrint = false;
+                        }
+                        for (int i = 0; i < lineMatch && i < lines.size(); 
i++) {
+                            context.out().println(lines.get(i));
+                        }
+                    }
+                    if (count) {
+                        context.out().println(nb);
+                    }
+                    match |= nb > 0;
+                }
+            }
+        }
+    }
+
+    public static void less(Context context, String[] argv) throws Exception {
+        Options opt = parseOptions(context, Less.usage(), argv);
+
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new Source.StdInSource(context.in()));
+            } else {
+                sources.addAll(maybeExpandGlob(context, arg).stream()
+                    .map(p -> {
+                        try {
+                            return new Source.URLSource(p.toUri().toURL(), 
p.toString());
+                        } catch (MalformedURLException e) {
+                            throw new RuntimeException(e);
+                        }
+                    })
+                    .collect(Collectors.toList()));
+            }
+        }
+
+        if (!context.isTty()) {
+            // Non-interactive mode - just cat the files
+            for (Source source : sources) {
+                try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(source.read()))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        context.out().println(line);
+                    }
+                }
+            }
+            return;
+        }
+
+        Less less = new Less(context.terminal(), context.currentDir(), opt);
+        less.run(sources);
+    }
+    private static class GrepSource {
+        private final InputStream inputStream;
+        private final Path path;
+        private final String name;
+
+        public GrepSource(InputStream inputStream, String name) {
+            this.inputStream = inputStream;
+            this.path = null;
+            this.name = name;
+        }
+
+        public GrepSource(Path path, String name) {
+            this.inputStream = null;
+            this.path = path;
+            this.name = name;
+        }
+
+        public InputStream getInputStream() throws IOException {
+            if (inputStream != null) {
+                return inputStream;
+            } else {
+                return path.toUri().toURL().openStream();
+            }
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    private static LinkOption[] getLinkOptions(boolean followLinks) {
+        if (followLinks) {
+            return EMPTY_LINK_OPTIONS;
+        } else { // return a clone that modifications to the array will not 
affect others
+            return NO_FOLLOW_OPTIONS.clone();
+        }
+    }
+
+    private static final LinkOption[] NO_FOLLOW_OPTIONS = new LinkOption[] 
{LinkOption.NOFOLLOW_LINKS};
+    private static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0];
+    private static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS =
+        Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd"));
+
+    private static boolean isWindowsExecutable(String fileName) {
+        if ((fileName == null) || (fileName.length() <= 0)) {
+            return false;
+        }
+        for (String suffix : WINDOWS_EXECUTABLE_EXTENSIONS) {
+            if (fileName.endsWith(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static Set<PosixFilePermission> getPermissionsFromFile(Path f) {
+        Set<PosixFilePermission> perms = new HashSet<>();
+        try {
+            perms = Files.getPosixFilePermissions(f);
+        } catch (IOException | UnsupportedOperationException ignore) {
+        }
+        if (OSUtils.IS_WINDOWS && 
isWindowsExecutable(f.getFileName().toString())) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+            perms.add(PosixFilePermission.GROUP_EXECUTE);
+            perms.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+        return perms;
+    }
+
+    private static List<Path> maybeExpandGlob(Context context, String s) {
+        if (s.contains("*") || s.contains("?")) {
+            return expandGlob(context, s);
+        }
+        return Collections.singletonList(context.currentDir().resolve(s));
+    }
+
+    private static List<Path> expandGlob(Context context, String pattern) {
+        Path path = Path.of(pattern);
+
+        Path base;
+        String globPart;
+
+        if (path.isAbsolute()) {
+            base = path.getParent();
+            globPart = path.getFileName().toString();
+        } else {
+            base = context.currentDir();
+            globPart = pattern;
+        }
+
+        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" 
+ globPart);
+
+        try {
+            return Files.list(base)
+                .filter(p -> matcher.matches(p.getFileName()))
+                .collect(Collectors.toList());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/SystemRegistryImpl.java
 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/SystemRegistryImpl.java
index 896130aef1..a39a6fa378 100644
--- 
a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/SystemRegistryImpl.java
+++ 
b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/SystemRegistryImpl.java
@@ -58,7 +58,8 @@ public class SystemRegistryImpl implements SystemRegistry {
         AND,
         OR,
         REDIRECT,
-        APPEND
+        APPEND,
+        PIPE
     }
 
     private static final Class<?>[] BUILTIN_REGISTRIES = {Builtins.class, 
ConsoleEngineImpl.class};
@@ -90,6 +91,7 @@ public class SystemRegistryImpl implements SystemRegistry {
         pipeName.put(Pipe.NAMED, "|");
         pipeName.put(Pipe.AND, "&&");
         pipeName.put(Pipe.OR, "||");
+        pipeName.put(Pipe.PIPE, "|!");
         pipeName.put(Pipe.REDIRECT, ">");
         pipeName.put(Pipe.APPEND, ">>");
         commandExecute.put("exit", new CommandMethods(this::exit, 
this::exitCompleter));
@@ -722,8 +724,9 @@ public class SystemRegistryImpl implements SystemRegistry {
                             }
                             pipe = words.get(i + 1);
                             if (!pipe.matches("\\w+") || 
!customPipes.containsKey(pipe)) {
-                                if (consoleOption("ignoreUnknownPipes", 
false)) break;
-                                throw new IllegalArgumentException("Unknown or 
illegal pipe name: " + pipe);
+                                if (!consoleOption("ignoreUnknownPipes", 
false)) {
+                                    throw new 
IllegalArgumentException("Unknown or illegal pipe name: " + pipe);
+                                }
                             }
                         }
                         pipes.add(pipe);
@@ -736,7 +739,7 @@ public class SystemRegistryImpl implements SystemRegistry {
                         }
                         break;
                     } else if (words.get(i).equals(pipeName.get(Pipe.OR))
-                            || words.get(i).equals(pipeName.get(Pipe.AND))) {
+                            || words.get(i).equals(pipeName.get(Pipe.AND))|| 
words.get(i).equals(pipeName.get(Pipe.PIPE))) {
                         if (variable != null || pipeSource != null) {
                             pipes.add(words.get(i));
                         } else if (pipes.size() > 0
@@ -1291,7 +1294,10 @@ public class SystemRegistryImpl implements 
SystemRegistry {
                         }
                         out = consoleEngine.execute(cmd.command(), 
cmd.rawLine(), cmd.args());
                     }
-                    if (cmd.pipe().equals(pipeName.get(Pipe.OR)) || 
cmd.pipe().equals(pipeName.get(Pipe.AND))) {
+                    if (cmd.pipe().equals(pipeName.get(Pipe.OR)) || 
cmd.pipe().equals(pipeName.get(Pipe.AND)) || 
cmd.pipe().equals(pipeName.get(Pipe.PIPE))) {
+                        if (cmd.pipe().equals(pipeName.get(Pipe.PIPE)) && out 
== null) {
+                            out = outputStream.output;
+                        }
                         ExecutionResult er = postProcess(cmd, statement, 
consoleEngine, out);
                         postProcessed = true;
                         consoleEngine.println(er.result());


Reply via email to