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

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

commit 482e28bf7726ca15d767ef4c79e7d8f87f9d17e1
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Aug 25 15:48:03 2025 +1000

    GROOVY-11742: posix commands should support variable assignment (amendments 
for /tail)
---
 .../groovy/org/apache/groovy/groovysh/Main.groovy  |  11 +-
 .../groovy/groovysh/jline/GroovyPosixCommands.java | 120 +++++++++++++++++----
 2 files changed, 109 insertions(+), 22 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 3d07e07472..84a8be0e53 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
@@ -82,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', '/wc', '/sort']
+    private static POSIX_FILE_CMDS = ['/wc', '/sort']
 
     @SuppressWarnings("resource")
     protected static class ExtraConsoleCommands extends JlineCommandRegistry 
implements CommandRegistry {
@@ -114,6 +114,7 @@ class Main {
                 '/ls'   : new CommandMethods((Function) this::ls, 
this::optFileCompleter),
                 '/grep' : new CommandMethods((Function) this::grepcmd, 
this::optFileCompleter),
                 '/head' : new CommandMethods((Function) this::headcmd, 
this::optFileCompleter),
+                '/tail' : new CommandMethods((Function) this::tailcmd, 
this::optFileCompleter),
                 '/cat'  : new CommandMethods((Function) this::cat, 
this::optFileCompleter),
                 "/!"    : new CommandMethods((Function) this::shell, 
this::defaultCompleter)
             ]
@@ -199,6 +200,14 @@ class Main {
             }
         }
 
+        private void tailcmd(CommandInput input) {
+            try {
+                GroovyPosixCommands.tail(context(input), ['/tail', 
*input.xargs()] as Object[])
+            } catch (Exception e) {
+                saveException(e)
+            }
+        }
+
         private GroovyPosixContext context(CommandInput input) {
             GroovyPosixContext ctx = new GroovyPosixContext(input.in(), 
input.out(), input.err(),
                 posix.context.currentDir(), input.terminal(), 
scriptEngine::get)
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
index 5cde0fcc24..dcf56c2446 100644
--- 
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
@@ -31,6 +31,7 @@ import org.jline.utils.OSUtils;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -115,15 +116,17 @@ public class GroovyPosixCommands extends PosixCommands {
     public static void head(Context context, Object[] argv) throws Exception {
         final String[] usage = {
             "/head - display first lines of files or variables",
-            "Usage: /head [-n lines | -c bytes] [file|variable ...]",
+            "Usage: /head [-n lines | -c bytes | -q | -v] [file|variable ...]",
             "  -? --help                    Show help",
             "  -n --lines=LINES             Print line counts",
             "  -c --bytes=BYTES             Print byte counts",
+            "  -q --quiet                   Never output filename headers",
+            "  -v --verbose                 Always output filename headers",
         };
         Options opt = parseOptions(context, usage, argv);
 
         if (opt.isSet("lines") && opt.isSet("bytes")) {
-            throw new IllegalArgumentException("usage: head [-n # | -c #] 
[file ...]");
+            throw new IllegalArgumentException("usage: /head [-n # | -c # | -q 
| -v] [file|variable ...]");
         }
 
         int nbLines = Integer.MAX_VALUE;
@@ -144,32 +147,107 @@ public class GroovyPosixCommands extends PosixCommands {
         boolean first = true;
         List<NamedInputStream> sources = getSources(context, argv, args);
         for (NamedInputStream nis : sources) {
-            if (!first && args.size() > 1) {
-                context.out().println();
+            boolean filenameHeader = sources.size() > 1;
+            if (opt.isSet("verbose")) {
+                filenameHeader = true;
+            } else if (opt.isSet("quiet")) {
+                filenameHeader = false;
             }
-            if (args.size() > 1) {
+            if (filenameHeader) {
+                if (!first) {
+                    context.out().println();
+                }
                 context.out().println("==> " + nis.getName() + " <==");
             }
+            doHead(context, nis.getInputStream(), nbLines, nbBytes);
+            first = false;
+        }
+    }
 
-            InputStream is = nis.getInputStream();
-            if (nbLines != Integer.MAX_VALUE) {
-                try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(is))) {
-                    String line;
-                    int count = 0;
-                    while ((line = reader.readLine()) != null && count < 
nbLines) {
-                        context.out().println(line);
-                        count++;
-                    }
+    private static void doHead(Context context, InputStream is, final int 
nbLines, final int nbBytes) throws IOException {
+        if (nbLines != Integer.MAX_VALUE) {
+            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(is))) {
+                String line;
+                int count = 0;
+                while ((line = reader.readLine()) != null && count < nbLines) {
+                    context.out().println(line);
+                    count++;
                 }
-            } else {
-                byte[] buffer = new byte[nbBytes];
-                int bytesRead = is.read(buffer);
-                if (bytesRead > 0) {
-                    context.out().write(buffer, 0, bytesRead);
+            }
+        } else {
+            byte[] buffer = new byte[nbBytes];
+            int bytesRead = is.read(buffer);
+            if (bytesRead > 0) {
+                context.out().write(buffer, 0, bytesRead);
+            }
+            is.close();
+        }
+    }
+
+    public static void tail(Context context, Object[] argv) throws Exception {
+        final String[] usage = {
+            "/tail - display last lines of files or variables",
+            "Usage: /tail [-n lines | -c bytes | -q | -v] [file|variable ...]",
+            "  -? --help                    Show help",
+            "  -n --lines=LINES             Number of lines to print",
+            "  -c --bytes=BYTES             Number of bytes to print",
+            "  -q --quiet                   Never output filename headers",
+            "  -v --verbose                 Always output filename headers",
+        };
+        Options opt = parseOptions(context, usage, argv);
+
+        if (opt.isSet("lines") && opt.isSet("bytes")) {
+            throw new IllegalArgumentException("usage: /tail [-c # | -n # | -q 
| -v] [file|variable ...]");
+        }
+
+        int lines = opt.isSet("lines") ? opt.getNumber("lines") : 10;
+        int bytes = opt.isSet("bytes") ? opt.getNumber("bytes") : -1;
+
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            args = Collections.singletonList("-");
+        }
+
+        List<NamedInputStream> sources = getSources(context, argv, args);
+        boolean filenameHeader = sources.size() > 1;
+        if (opt.isSet("verbose")) {
+            filenameHeader = true;
+        } else if (opt.isSet("quiet")) {
+            filenameHeader = false;
+        }
+        for (NamedInputStream nis : sources) {
+            if (filenameHeader) {
+                context.out().println("==> " + nis.getName() + " <==");
+            }
+            tailInputStream(context, nis.getInputStream(), lines, bytes);
+        }
+    }
+
+    private static void tailInputStream(Context context, InputStream is, int 
lines, int bytes) throws IOException {
+        if (bytes > 0) {
+            // Read all and keep last bytes
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            byte[] buffer = new byte[8192];
+            int n;
+            while ((n = is.read(buffer)) != -1) {
+                baos.write(buffer, 0, n);
+            }
+            byte[] data = baos.toByteArray();
+            int start = Math.max(0, data.length - bytes);
+            context.out().write(data, start, data.length - start);
+        } else {
+            // Read all and keep last lines
+            List<String> allLines = new ArrayList<>();
+            try (BufferedReader reader = new BufferedReader(new 
java.io.InputStreamReader(is))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    allLines.add(line);
                 }
-                is.close();
             }
-            first = false;
+            int start = Math.max(0, allLines.size() - lines);
+            for (int i = start; i < allLines.size(); i++) {
+                context.out().println(allLines.get(i));
+            }
         }
     }
 

Reply via email to