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)); + } } }