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());