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 b352cc95b3 GROOVY-11742: posix commands should support variable assignment (for for /wc and /sort) b352cc95b3 is described below commit b352cc95b32d35fc72968cfe276c34b89c8e9d29 Author: Paul King <pa...@asert.com.au> AuthorDate: Tue Aug 26 22:23:34 2025 +1000 GROOVY-11742: posix commands should support variable assignment (for for /wc and /sort) --- .../groovy/org/apache/groovy/groovysh/Main.groovy | 64 ++---- .../groovy/groovysh/jline/GroovyPosixCommands.java | 248 ++++++++++++++++++--- 2 files changed, 229 insertions(+), 83 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 84a8be0e53..cca9464657 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,8 @@ 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 = ['/wc', '/sort'] +// private static POSIX_CMDS = [] + private static GROOVY_POSIX_CMDS = ['/ls', '/wc', '/sort', '/head', '/tail', '/cat', '/grep'] @SuppressWarnings("resource") protected static class ExtraConsoleCommands extends JlineCommandRegistry implements CommandRegistry { @@ -111,19 +112,20 @@ 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), - '/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) ] - POSIX_FILE_CMDS.each { String cmd -> - String orig = cmd[1..-1] - usage[cmd] = adjustUsage(orig, cmd) - posix.register(cmd, PosixCommands::"$orig") - cmds.put(cmd, new CommandMethods((Function) this::posix, this::optFileCompleter)) + GROOVY_POSIX_CMDS.each { String cmd -> + String base = cmd[1..-1] + usage[cmd] = adjustUsage(base, cmd) + posix.register(cmd, PosixCommands::"$base") + cmds.put(cmd, new CommandMethods((Function) this::posixCommand, this::optFileCompleter)) } +// POSIX_CMDS.each { String cmd -> +// String orig = cmd[1..-1] +// usage[cmd] = adjustUsage(orig, cmd) +// posix.register(cmd, PosixCommands::"$orig") +// cmds.put(cmd, new CommandMethods((Function) this::posix, this::optFileCompleter)) +// } posix.register('cd', PosixCommands::cd) posix.register('/cd', PosixCommands::cd) posix.register('/pwd', PosixCommands::pwd) @@ -176,33 +178,11 @@ class Main { } } - private void ls(CommandInput input) { + private void posixCommand(CommandInput input) { try { - GroovyPosixCommands.ls(context(input), ['/ls', *input.args()] as String[]) - } catch (Exception e) { - saveException(e) - } - } - - private void grepcmd(CommandInput input) { - try { - GroovyPosixCommands.grep(context(input), ['/grep', *input.xargs()] as Object[]) - } catch (Exception e) { - saveException(e) - } - } - - private void headcmd(CommandInput input) { - try { - GroovyPosixCommands.head(context(input), ['/head', *input.xargs()] as Object[]) - } catch (Exception e) { - saveException(e) - } - } - - private void tailcmd(CommandInput input) { - try { - GroovyPosixCommands.tail(context(input), ['/tail', *input.xargs()] as Object[]) + String cmd = input.command() + String name = cmd[1..-1] + GroovyPosixCommands."$name"(context(input), [cmd, *input.xargs()] as Object[]) } catch (Exception e) { saveException(e) } @@ -214,14 +194,6 @@ class Main { ctx } - private void cat(CommandInput input) { - try { - GroovyPosixCommands.cat(context(input), ['/cat', *input.xargs()] as Object[]) - } catch (Exception e) { - saveException(e) - } - } - private void date(CommandInput input) { posix(adjustUsage('date', '/date'), input) } @@ -448,7 +420,7 @@ class Main { if (!OSUtils.IS_WINDOWS) { setSpecificHighlighter("/!", SyntaxHighlighter.build(jnanorc, "SH-REPL")) } - addFileHighlight('/nano', '/less', '/slurp', '/load', '/save', *POSIX_FILE_CMDS, '/cd', '/ls', '/cat', '/grep') + addFileHighlight('/nano', '/less', '/slurp', '/load', '/save', *GROOVY_POSIX_CMDS, '/cd') 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/GroovyPosixCommands.java b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommands.java index 778d22bc9c..4dbd281a86 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 @@ -40,6 +40,7 @@ import org.jline.utils.OSUtils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -121,10 +122,117 @@ public class GroovyPosixCommands extends PosixCommands { } } + public static void wc(Context context, Object[] argv) throws Exception { + final String[] usage = { + "/wc - word, line, character, and byte count", + "Usage: /wc [OPTIONS] [FILES]", + " -? --help Show help", + " -l --lines Print line counts", + " -c --bytes Print byte counts", + " -m --chars Print character counts", + " -w --words Print word counts", + " --total=WHEN Print total counts, WHEN=auto|always|never|only", + }; + Options opt = parseOptions(context, usage, argv); + + List<String> args = opt.args(); + if (args.isEmpty()) { + args = Collections.singletonList("-"); + } + List<NamedInputStream> sources = getSources(context, argv, args); + + boolean showLines = opt.isSet("lines"); + boolean showWords = opt.isSet("words"); + boolean showChars = opt.isSet("chars"); + boolean showBytes = opt.isSet("bytes"); + boolean only = false; + boolean total = sources.size() > 1; + String totalOpt = opt.isSet("total") ? opt.get("total") : "auto"; + switch (totalOpt) { + case "always": + case "yes": + case "force": + total = true; + break; + case "never": + case "no": + case "none": + total = false; + break; + case "only": + only = true; + break; + case "auto": + case "tty": + case "if-tty": + total = context.isTty(); + break; + default: + throw new IllegalArgumentException("invalid argument '" + totalOpt + "' for '--total'"); + } + + // If no options specified, show all + if (!showLines && !showWords && !showChars && !showBytes) { + showLines = showWords = showBytes = true; + } + + long totalLines = 0, totalWords = 0, totalChars = 0, totalBytes = 0; + + for (NamedInputStream source : sources) { + long lines = 0, words = 0, chars = 0, bytes = 0; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(source.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + lines++; + chars += line.length() + 1; // +1 for newline + bytes += line.getBytes().length + 1; // +1 for newline + + // Count words + String[] wordArray = line.trim().split("\\s+"); + if (wordArray.length == 1 && wordArray[0].isEmpty()) { + // Empty line + } else { + words += wordArray.length; + } + } + } + + totalLines += lines; + totalWords += words; + totalChars += chars; + totalBytes += bytes; + + if (only) continue; + // Print results for this file + StringBuilder result = new StringBuilder(); + if (showLines) result.append(String.format("%8d", lines)); + if (showWords) result.append(String.format("%8d", words)); + if (showChars) result.append(String.format("%8d", chars)); + if (showBytes) result.append(String.format("%8d", bytes)); + result.append(" ").append(source.getName()); + + context.out().println(result); + context.out().flush(); + } + + // Print totals if multiple files + if (total) { + StringBuilder result = new StringBuilder(); + if (showLines) result.append(String.format("%8d", totalLines)); + if (showWords) result.append(String.format("%8d", totalWords)); + if (showChars) result.append(String.format("%8d", totalChars)); + if (showBytes) result.append(String.format("%8d", totalBytes)); + result.append(" total"); + + context.out().println(result); + } + } + 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 | -q | -v] [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", @@ -134,7 +242,11 @@ public class GroovyPosixCommands extends PosixCommands { Options opt = parseOptions(context, usage, argv); if (opt.isSet("lines") && opt.isSet("bytes")) { - throw new IllegalArgumentException("usage: /head [-n # | -c # | -q | -v] [file|variable ...]"); + throw new IllegalArgumentException("usage: /head [-n # | -c #] [-q | -v] [file|variable ...]"); + } + + if (opt.isSet("quiet") && opt.isSet("verbose")) { + throw new IllegalArgumentException("usage: /head [-n # | -c #] [-q | -v] [file|variable ...]"); } int nbLines = Integer.MAX_VALUE; @@ -195,7 +307,7 @@ public class GroovyPosixCommands extends PosixCommands { 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 ...]", + "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", @@ -205,7 +317,11 @@ public class GroovyPosixCommands extends PosixCommands { Options opt = parseOptions(context, usage, argv); if (opt.isSet("lines") && opt.isSet("bytes")) { - throw new IllegalArgumentException("usage: /tail [-c # | -n # | -q | -v] [file|variable ...]"); + throw new IllegalArgumentException("usage: /tail [-c # | -n #] [-q | -v] [file|variable ...]"); + } + + if (opt.isSet("quiet") && opt.isSet("verbose")) { + throw new IllegalArgumentException("usage: /tail [-c # | -n #] [-q | -v] [file|variable ...]"); } int lines = opt.isSet("lines") ? opt.getNumber("lines") : 10; @@ -259,15 +375,7 @@ public class GroovyPosixCommands extends PosixCommands { } } - private static InputStream newInputStream(Path p) { - try { - return Files.newInputStream(p); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static void ls(Context context, String[] argv) throws Exception { + public static void ls(Context context, Object[] argv) throws Exception { final String[] usage = { "/ls - list files", "Usage: /ls [OPTIONS] [PATTERNS...]", @@ -891,26 +999,51 @@ public class GroovyPosixCommands extends PosixCommands { } } - private static List<NamedInputStream> getSources(Context context, Object[] argv, List<String> args) { - List<NamedInputStream> sources = new ArrayList<>(); - for (String arg : args) { - if ("-".equals(arg)) { - sources.add(new NamedInputStream(context.in(), "(standard input)")); - } else if (arg.startsWith("[Ljava.lang.String;@")) { - sources.add(new NamedInputStream(variableInputStream(argv, arg), arg)); - } else { - sources.addAll(maybeExpandGlob(context, arg) - .map(gp -> new NamedInputStream(newInputStream(gp), gp.toString())) - .collect(Collectors.toList())); + public static void sort(Context context, Object[] argv) throws Exception { + final String[] usage = { + "/sort - writes sorted standard input to standard output.", + "Usage: /sort [OPTIONS] [FILES]", + " -? --help show help", + " -f --ignore-case fold lower case to upper case characters", + " -r --reverse reverse the result of comparisons", + " -u --unique output only the first of an equal run", + " -t --field-separator=SEP use SEP instead of non-blank to blank transition", + " -b --ignore-leading-blanks ignore leading blancks", + " --numeric-sort compare according to string numerical value", + " -k --key=KEY fields to use for sorting separated by whitespaces" + }; + + Options opt = parseOptions(context, usage, argv); + + List<String> args = opt.args(); + if (args.isEmpty()) { + args = Collections.singletonList("-"); + } + List<NamedInputStream> sources = getSources(context, argv, args); + List<String> lines = new ArrayList<>(); + for (NamedInputStream s : sources) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()))) { + readLines(reader, lines); } } - return sources; - } - private static ByteArrayInputStream variableInputStream(Object[] argv, String arg) { - String[] found = (String[]) Arrays.stream(argv).filter(v -> v.toString().equals(arg)).findFirst().get(); - ByteArrayInputStream inputStream = new ByteArrayInputStream(ArrayGroovyMethods.join(found, "\n").getBytes(StandardCharsets.UTF_8)); - return inputStream; + String separator = opt.get("field-separator"); + boolean caseInsensitive = opt.isSet("ignore-case"); + boolean reverse = opt.isSet("reverse"); + boolean ignoreBlanks = opt.isSet("ignore-leading-blanks"); + boolean numeric = opt.isSet("numeric-sort"); + boolean unique = opt.isSet("unique"); + List<String> sortFields = opt.getList("key"); + + char sep = (separator == null || separator.length() == 0) ? '\0' : separator.charAt(0); + lines.sort(new SortComparator(caseInsensitive, reverse, ignoreBlanks, numeric, sep, sortFields)); + String last = null; + for (String s : lines) { + if (!unique || last == null || !s.equals(last)) { + context.out().println(s); + } + last = s; + } } public static void less(Context context, String[] argv) throws Exception { @@ -953,34 +1086,68 @@ public class GroovyPosixCommands extends PosixCommands { less.run(sources); } - private static class NamedInputStream { - private final InputStream inputStream; + private static class NamedInputStream implements Closeable { + private InputStream inputStream; private final Path path; private final String name; + private final boolean close; - public NamedInputStream(InputStream inputStream, String name) { + public NamedInputStream(InputStream inputStream, String name, boolean close) { this.inputStream = inputStream; this.path = null; this.name = name; + this.close = close; } + public NamedInputStream(InputStream inputStream, String name) { + this(inputStream, name, true); + } public NamedInputStream(Path path, String name) { this.inputStream = null; this.path = path; this.name = name; + this.close = false; } public InputStream getInputStream() throws IOException { - if (inputStream != null) { - return inputStream; - } else { - return path.toUri().toURL().openStream(); + if (inputStream == null) { + inputStream = path.toUri().toURL().openStream(); } + return inputStream; } public String getName() { return name; } + + @Override + public void close() throws IOException { + if (inputStream != null && close) { + inputStream.close(); + } + } + } + + private static List<NamedInputStream> getSources(Context context, Object[] argv, List<String> args) { + List<NamedInputStream> sources = new ArrayList<>(); + for (String arg : args) { + if ("-".equals(arg)) { + sources.add(new NamedInputStream(context.in(), "(standard input)", false)); + } else if (arg.startsWith("[Ljava.lang.String;@")) { + sources.add(new NamedInputStream(variableInputStream(argv, arg), arg)); + } else { + sources.addAll(maybeExpandGlob(context, arg) + .map(p -> new NamedInputStream(p, p.toString())) + .collect(Collectors.toList())); + } + } + return sources; + } + + private static ByteArrayInputStream variableInputStream(Object[] argv, String arg) { + String[] found = (String[]) Arrays.stream(argv).filter(v -> v.toString().equals(arg)).findFirst().get(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(ArrayGroovyMethods.join(found, "\n").getBytes(StandardCharsets.UTF_8)); + return inputStream; } private static LinkOption[] getLinkOptions(boolean followLinks) { @@ -1022,6 +1189,13 @@ public class GroovyPosixCommands extends PosixCommands { return perms; } + private static void readLines(BufferedReader reader, List<String> lines) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + private static Stream<Path> maybeExpandGlob(Context context, String s) { if (s.contains("*") || s.contains("?")) { return expandGlob(context, s).stream();