| /* |
| * 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.felix.gogo.jline; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FilterInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.io.Reader; |
| import java.nio.file.Files; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Path; |
| import java.nio.file.StandardWatchEventKinds; |
| import java.nio.file.WatchKey; |
| import java.nio.file.WatchService; |
| import java.nio.file.attribute.FileTime; |
| import java.nio.file.attribute.PosixFilePermission; |
| import java.nio.file.attribute.PosixFilePermissions; |
| import java.text.SimpleDateFormat; |
| 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.Comparator; |
| import java.util.Date; |
| 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.concurrent.Executors; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Consumer; |
| import java.util.function.IntBinaryOperator; |
| import java.util.function.IntConsumer; |
| 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; |
| |
| import org.apache.felix.service.command.Process; |
| import org.apache.felix.gogo.jline.Shell.Context; |
| import org.apache.felix.service.command.CommandProcessor; |
| import org.apache.felix.service.command.CommandSession; |
| import org.jline.builtins.Commands; |
| import org.jline.builtins.Less; |
| import org.jline.builtins.Nano; |
| import org.jline.builtins.Options; |
| import org.jline.builtins.Source; |
| import org.jline.builtins.Source.PathSource; |
| import org.jline.builtins.TTop; |
| import org.jline.terminal.Attributes; |
| import org.jline.terminal.Terminal; |
| import org.jline.utils.AttributedString; |
| import org.jline.utils.AttributedStringBuilder; |
| import org.jline.utils.InfoCmp.Capability; |
| import org.jline.utils.OSUtils; |
| import org.jline.utils.StyleResolver; |
| |
| /** |
| * Posix-like utilities. |
| * |
| * @see <a href="http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html"> |
| * http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html</a> |
| */ |
| public class Posix { |
| |
| static final String[] functions; |
| |
| static { |
| // TTop function is new in JLine 3.2 |
| String[] func; |
| try { |
| @SuppressWarnings("unused") |
| Class<?> cl = TTop.class; |
| func = new String[] { |
| "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls", |
| "less", "watch", "nano", "tmux", |
| "head", "tail", "clear", "wc", |
| "date", "ttop", |
| }; |
| } catch (Throwable t) { |
| func = new String[] { |
| "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls", |
| "less", "watch", "nano", "tmux", |
| "head", "tail", "clear", "wc", |
| "date" |
| }; |
| } |
| functions = func; |
| } |
| |
| public static final String DEFAULT_LS_COLORS = "dr=1;91:ex=1;92:sl=1;96:ot=34;43"; |
| public static final String DEFAULT_GREP_COLORS = "mt=1;31:fn=35:ln=32:se=36"; |
| |
| private static final LinkOption[] NO_FOLLOW_OPTIONS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; |
| private static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd")); |
| private static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0]; |
| |
| private final CommandProcessor processor; |
| |
| public Posix(CommandProcessor processor) { |
| this.processor = processor; |
| } |
| |
| public void _main(CommandSession session, String[] argv) { |
| if (argv == null || argv.length < 1) { |
| throw new IllegalArgumentException(); |
| } |
| Process process = Process.Utils.current(); |
| try { |
| run(session, process, argv); |
| } catch (IllegalArgumentException e) { |
| process.err().println(e.getMessage()); |
| process.error(2); |
| } catch (HelpException e) { |
| process.err().println(e.getMessage()); |
| process.error(0); |
| } catch (Exception e) { |
| process.err().println(argv[0] + ": " + e.toString()); |
| process.error(1); |
| } |
| } |
| |
| @SuppressWarnings("serial") |
| protected static class HelpException extends Exception { |
| public HelpException(String message) { |
| super(message); |
| } |
| } |
| |
| protected Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws Exception { |
| Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true); |
| if (opt.isSet("help")) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| opt.usage(new PrintStream(baos)); |
| throw new HelpException(baos.toString()); |
| } |
| return opt; |
| } |
| |
| protected String get(CommandSession session, String name) { |
| Object o = session.get(name); |
| return o != null ? o.toString() : null; |
| } |
| |
| protected Object run(CommandSession session, Process process, String[] argv) throws Exception { |
| switch (argv[0]) { |
| case "cat": |
| cat(session, process, argv); |
| break; |
| case "echo": |
| echo(session, process, argv); |
| break; |
| case "grep": |
| grep(session, process, argv); |
| break; |
| case "sort": |
| sort(session, process, argv); |
| break; |
| case "sleep": |
| sleep(session, process, argv); |
| break; |
| case "cd": |
| cd(session, process, argv); |
| break; |
| case "pwd": |
| pwd(session, process, argv); |
| break; |
| case "ls": |
| ls(session, process, argv); |
| break; |
| case "less": |
| less(session, process, argv); |
| break; |
| case "watch": |
| watch(session, process, argv); |
| break; |
| case "nano": |
| nano(session, process, argv); |
| break; |
| case "tmux": |
| tmux(session, process, argv); |
| break; |
| case "ttop": |
| ttop(session, process, argv); |
| break; |
| case "clear": |
| clear(session, process, argv); |
| break; |
| case "head": |
| head(session, process, argv); |
| break; |
| case "tail": |
| tail(session, process, argv); |
| break; |
| case "wc": |
| wc(session, process, argv); |
| break; |
| case "date": |
| date(session, process, argv); |
| break; |
| } |
| return null; |
| } |
| |
| protected void date(CommandSession session, Process process, String[] argv) throws Exception { |
| String[] usage = { |
| "date - display date", |
| "Usage: date [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]", |
| " -? --help Show help", |
| " -u Use UTC", |
| " -r Print the date represented by 'seconds' since January 1, 1970", |
| " -f Use 'input_fmt' to parse 'new_date'" |
| }; |
| Date input = new Date(); |
| String output = null; |
| for (int i = 1; i < argv.length; i++) { |
| if ("-?".equals(argv[i]) || "--help".equals(argv[i])) { |
| throw new HelpException(String.join("\n", usage)); |
| } |
| else if ("-r".equals(argv[i])) { |
| if (i + 1 < argv.length) { |
| input = new Date(Long.parseLong(argv[++i]) * 1000L); |
| } else { |
| throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); |
| } |
| } |
| else if ("-f".equals(argv[i])) { |
| if (i + 2 < argv.length) { |
| String fmt = argv[++i]; |
| String inp = argv[++i]; |
| String jfmt = toJavaDateFormat(fmt); |
| input = new SimpleDateFormat(jfmt).parse(inp); |
| } else { |
| throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); |
| } |
| } |
| else if (argv[i].startsWith("+")) { |
| if (output == null) { |
| output = argv[i].substring(1); |
| } else { |
| throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); |
| } |
| } |
| else { |
| throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); |
| } |
| } |
| if (output == null) { |
| output = "%c"; |
| } |
| // Print output |
| process.out().println(new SimpleDateFormat(toJavaDateFormat(output)).format(input)); |
| } |
| |
| private String toJavaDateFormat(String format) { |
| // transform Unix format to Java SimpleDateFormat (if required) |
| StringBuilder sb = new StringBuilder(); |
| boolean quote = false; |
| for (int i = 0; i < format.length(); i++) { |
| char c = format.charAt(i); |
| if (c == '%') { |
| if (i + 1 < format.length()) { |
| if (quote) { |
| sb.append('\''); |
| quote = false; |
| } |
| c = format.charAt(++i); |
| switch (c) { |
| case '+': |
| case 'A': sb.append("MMM EEE d HH:mm:ss yyyy"); break; |
| case 'a': sb.append("EEE"); break; |
| case 'B': sb.append("MMMMMMM"); break; |
| case 'b': sb.append("MMM"); break; |
| case 'C': sb.append("yy"); break; |
| case 'c': sb.append("MMM EEE d HH:mm:ss yyyy"); break; |
| case 'D': sb.append("MM/dd/yy"); break; |
| case 'd': sb.append("dd"); break; |
| case 'e': sb.append("dd"); break; |
| case 'F': sb.append("yyyy-MM-dd"); break; |
| case 'G': sb.append("YYYY"); break; |
| case 'g': sb.append("YY"); break; |
| case 'H': sb.append("HH"); break; |
| case 'h': sb.append("MMM"); break; |
| case 'I': sb.append("hh"); break; |
| case 'j': sb.append("DDD"); break; |
| case 'k': sb.append("HH"); break; |
| case 'l': sb.append("hh"); break; |
| case 'M': sb.append("mm"); break; |
| case 'm': sb.append("MM"); break; |
| case 'N': sb.append("S"); break; |
| case 'n': sb.append("\n"); break; |
| case 'P': sb.append("aa"); break; |
| case 'p': sb.append("aa"); break; |
| case 'r': sb.append("hh:mm:ss aa"); break; |
| case 'R': sb.append("HH:mm"); break; |
| case 'S': sb.append("ss"); break; |
| case 's': sb.append("S"); break; |
| case 'T': sb.append("HH:mm:ss"); break; |
| case 't': sb.append("\t"); break; |
| case 'U': sb.append("w"); break; |
| case 'u': sb.append("u"); break; |
| case 'V': sb.append("W"); break; |
| case 'v': sb.append("dd-MMM-yyyy"); break; |
| case 'W': sb.append("w"); break; |
| case 'w': sb.append("u"); break; |
| case 'X': sb.append("HH:mm:ss"); break; |
| case 'x': sb.append("MM/dd/yy"); break; |
| case 'Y': sb.append("yyyy"); break; |
| case 'y': sb.append("yy"); break; |
| case 'Z': sb.append("z"); break; |
| case 'z': sb.append("X"); break; |
| case '%': sb.append("%"); break; |
| } |
| } else { |
| if (!quote) { |
| sb.append('\''); |
| } |
| sb.append(c); |
| sb.append('\''); |
| } |
| } else { |
| if ((c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') && !quote) { |
| sb.append('\''); |
| quote = true; |
| } |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| protected void wc(CommandSession session, Process process, String[] argv) throws Exception { |
| 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", |
| }; |
| Options opt = parseOptions(session, 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 StdInSource(process)); |
| } else { |
| sources.add(new PathSource(session.currentDir().resolve(arg), arg)); |
| } |
| } |
| boolean displayLines = opt.isSet("lines"); |
| boolean displayWords = opt.isSet("words"); |
| boolean displayChars = opt.isSet("chars"); |
| boolean displayBytes = opt.isSet("bytes"); |
| if (!displayLines && !displayWords && !displayChars && !displayBytes) { |
| displayLines = true; |
| displayWords = true; |
| displayBytes = true; |
| } |
| String format = ""; |
| if (displayLines) { |
| if (!displayBytes && !displayChars && !displayWords) { |
| format = "%1$d"; |
| } else { |
| format += "%1$8d"; |
| } |
| } |
| if (displayWords) { |
| if (!displayLines && !displayBytes && !displayChars) { |
| format = "%2$d"; |
| } else { |
| format += "%2$8d"; |
| } |
| } |
| if (displayChars) { |
| if (!displayLines && !displayBytes && !displayWords) { |
| format = "%3$d"; |
| } else { |
| format += "%3$8d"; |
| } |
| } |
| if (displayBytes) { |
| if (!displayLines && !displayChars && !displayWords) { |
| format = "%4$d"; |
| } else { |
| format += "%4$8d"; |
| } |
| } |
| if (sources.size() > 1 || (sources.size() == 1 && sources.get(0).getName() != null)) { |
| format += " %5$8s"; |
| } |
| int totalLines = 0; |
| int totalBytes = 0; |
| int totalChars = 0; |
| int totalWords = 0; |
| for (Source src : sources) { |
| try (InputStream is = src.read()) { |
| AtomicInteger lines = new AtomicInteger(); |
| AtomicInteger bytes = new AtomicInteger(); |
| AtomicInteger chars = new AtomicInteger(); |
| AtomicInteger words = new AtomicInteger(); |
| AtomicBoolean inWord = new AtomicBoolean(); |
| AtomicBoolean lastNl = new AtomicBoolean(true); |
| InputStream isc = new FilterInputStream(is) { |
| @Override |
| public int read() throws IOException { |
| int b = super.read(); |
| if (b >= 0) { |
| bytes.incrementAndGet(); |
| } |
| return b; |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| int nb = super.read(b, off, len); |
| if (nb > 0) { |
| bytes.addAndGet(nb); |
| } |
| return nb; |
| } |
| }; |
| IntConsumer consumer = cp -> { |
| chars.incrementAndGet(); |
| boolean ws = Character.isWhitespace(cp); |
| if (inWord.getAndSet(!ws) && ws) { |
| words.incrementAndGet(); |
| } |
| if (cp == '\n') { |
| lines.incrementAndGet(); |
| lastNl.set(true); |
| } else { |
| lastNl.set(false); |
| } |
| }; |
| Reader reader = new InputStreamReader(isc); |
| while (true) { |
| int h = reader.read(); |
| if (Character.isHighSurrogate((char) h)) { |
| int l = reader.read(); |
| if (Character.isLowSurrogate((char) l)) { |
| int cp = Character.toCodePoint((char) h, (char) l); |
| consumer.accept(cp); |
| } else { |
| consumer.accept(h); |
| if (l >= 0) { |
| consumer.accept(l); |
| } else { |
| break; |
| } |
| } |
| } else if (h >= 0) { |
| consumer.accept(h); |
| } else { |
| break; |
| } |
| } |
| if (inWord.get()) { |
| words.incrementAndGet(); |
| } |
| if (!lastNl.get()) { |
| lines.incrementAndGet(); |
| } |
| process.out().println(String.format(format, lines.get(), words.get(), chars.get(), bytes.get(), src.getName())); |
| totalBytes += bytes.get(); |
| totalChars += chars.get(); |
| totalWords += words.get(); |
| totalLines += lines.get(); |
| } |
| } |
| if (sources.size() > 1) { |
| process.out().println(String.format(format, totalLines, totalWords, totalChars, totalBytes, "total")); |
| } |
| } |
| |
| protected void head(CommandSession session, Process process, String[] argv) throws Exception { |
| String[] usage = { |
| "head - displays first lines of file", |
| "Usage: head [-n lines | -c bytes] [file ...]", |
| " -? --help Show help", |
| " -n --lines=LINES Print line counts", |
| " -c --bytes=BYTES Print byte counts", |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| if (opt.isSet("lines") && opt.isSet("bytes")) { |
| throw new IllegalArgumentException("usage: head [-n # | -c #] [file ...]"); |
| } |
| int nbLines = Integer.MAX_VALUE; |
| int nbBytes = Integer.MAX_VALUE; |
| if (opt.isSet("lines")) { |
| nbLines = opt.getNumber("lines"); |
| } else if (opt.isSet("bytes")) { |
| nbBytes = opt.getNumber("bytes"); |
| } else { |
| nbLines = 10; |
| } |
| List<Source> sources = new ArrayList<>(); |
| if (opt.args().isEmpty()) { |
| opt.args().add("-"); |
| } |
| for (String arg : opt.args()) { |
| if ("-".equals(arg)) { |
| sources.add(new StdInSource(process)); |
| } else { |
| sources.add(new PathSource(session.currentDir().resolve(arg), arg)); |
| } |
| } |
| for (Source src : sources) { |
| int bytes = nbBytes; |
| int lines = nbLines; |
| if (sources.size() > 1) { |
| if (src != sources.get(0)) { |
| process.out().println(); |
| } |
| process.out().println("==> " + src.getName() + " <=="); |
| } |
| try (InputStream is = src.read()) { |
| byte[] buf = new byte[1024]; |
| int nb; |
| do { |
| nb = is.read(buf); |
| if (nb > 0 && lines > 0 && bytes > 0) { |
| nb = Math.min(nb, bytes); |
| for (int i = 0; i < nb; i++) { |
| if (buf[i] == '\n' && --lines <= 0) { |
| nb = i + 1; |
| break; |
| } |
| } |
| bytes -= nb; |
| process.out().write(buf, 0, nb); |
| } |
| } while (nb > 0 && lines > 0 && bytes > 0); |
| } |
| } |
| } |
| |
| protected void tail(CommandSession session, Process process, String[] argv) throws Exception { |
| String[] usage = { |
| "tail - displays last lines of file", |
| "Usage: tail [-f] [-q] [-c # | -n #] [file ...]", |
| " -? --help Show help", |
| " -q --quiet Suppress headers when printing multiple sources", |
| " -f --follow Do not stop at end of file", |
| " -F --FOLLOW Follow and check for file renaming or rotation", |
| " -n --lines=LINES Number of lines to print", |
| " -c --bytes=BYTES Number of bytes to print", |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| if (opt.isSet("lines") && opt.isSet("bytes")) { |
| throw new IllegalArgumentException("usage: tail [-f] [-q] [-c # | -n #] [file ...]"); |
| } |
| int lines; |
| int bytes; |
| if (opt.isSet("lines")) { |
| lines = opt.getNumber("lines"); |
| bytes = Integer.MAX_VALUE; |
| } else if (opt.isSet("bytes")) { |
| lines = Integer.MAX_VALUE; |
| bytes = opt.getNumber("bytes"); |
| } else { |
| lines = 10; |
| bytes = Integer.MAX_VALUE; |
| } |
| boolean follow = opt.isSet("follow") || opt.isSet("FOLLOW"); |
| |
| AtomicReference<Object> lastPrinted = new AtomicReference<>(); |
| WatchService watchService = follow ? session.currentDir().getFileSystem().newWatchService() : null; |
| Set<Path> watched = new HashSet<>(); |
| |
| class Input implements Closeable { |
| String name; |
| Path path; |
| Reader reader; |
| StringBuilder buffer; |
| long ino; |
| long size; |
| |
| public Input(String name) { |
| this.name = name; |
| this.buffer = new StringBuilder(); |
| } |
| |
| public void open() { |
| if (reader == null) { |
| try { |
| InputStream is; |
| if ("-".equals(name)) { |
| is = new StdInSource(process).read(); |
| } else { |
| path = session.currentDir().resolve(name); |
| is = Files.newInputStream(path); |
| if (opt.isSet("FOLLOW")) { |
| try { |
| ino = (Long) Files.getAttribute(path, "unix:ino"); |
| } catch (Exception e) { |
| // Ignore |
| } |
| } |
| size = Files.size(path); |
| } |
| reader = new InputStreamReader(is); |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (reader != null) { |
| try { |
| reader.close(); |
| } finally { |
| reader = null; |
| } |
| } |
| } |
| |
| public boolean tail() throws IOException { |
| open(); |
| if (reader != null) { |
| if (buffer != null) { |
| char[] buf = new char[1024]; |
| int nb; |
| while ((nb = reader.read(buf)) > 0) { |
| buffer.append(buf, 0, nb); |
| if (bytes > 0 && buffer.length() > bytes) { |
| buffer.delete(0, buffer.length() - bytes); |
| } else { |
| int l = 0; |
| int i = -1; |
| while ((i = buffer.indexOf("\n", i + 1)) >= 0) { |
| l++; |
| } |
| if (l > lines) { |
| i = -1; |
| l = l - lines; |
| while (--l >= 0) { |
| i = buffer.indexOf("\n", i + 1); |
| } |
| buffer.delete(0, i + 1); |
| } |
| } |
| } |
| String toPrint = buffer.toString(); |
| print(toPrint); |
| buffer = null; |
| if (follow && path != null) { |
| Path parent = path.getParent(); |
| if (!watched.contains(parent)) { |
| parent.register(watchService, |
| StandardWatchEventKinds.ENTRY_CREATE, |
| StandardWatchEventKinds.ENTRY_DELETE, |
| StandardWatchEventKinds.ENTRY_MODIFY); |
| watched.add(parent); |
| } |
| } |
| return follow; |
| } |
| else if (follow && path != null) { |
| while (true) { |
| long newSize = Files.size(path); |
| if (size != newSize) { |
| char[] buf = new char[1024]; |
| int nb; |
| while ((nb = reader.read(buf)) > 0) { |
| print(new String(buf, 0, nb)); |
| } |
| size = newSize; |
| } |
| if (opt.isSet("FOLLOW")) { |
| long newIno = 0; |
| try { |
| newIno = (Long) Files.getAttribute(path, "unix:ino"); |
| } catch (Exception e) { |
| // Ignore |
| } |
| if (ino != newIno) { |
| close(); |
| open(); |
| ino = newIno; |
| size = -1; |
| continue; |
| } |
| } |
| break; |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } else { |
| Path parent = path.getParent(); |
| if (!watched.contains(parent)) { |
| parent.register(watchService, |
| StandardWatchEventKinds.ENTRY_CREATE, |
| StandardWatchEventKinds.ENTRY_DELETE, |
| StandardWatchEventKinds.ENTRY_MODIFY); |
| watched.add(parent); |
| } |
| return true; |
| } |
| } |
| |
| private void print(String toPrint) { |
| if (lastPrinted.get() != this && opt.args().size() > 1 && !opt.isSet("quiet")) { |
| process.out().println(); |
| process.out().println("==> " + name + " <=="); |
| } |
| process.out().print(toPrint); |
| lastPrinted.set(this); |
| } |
| } |
| |
| if (opt.args().isEmpty()) { |
| opt.args().add("-"); |
| } |
| List<Input> inputs = new ArrayList<>(); |
| for (String name : opt.args()) { |
| Input input = new Input(name); |
| inputs.add(input); |
| } |
| try { |
| boolean cont = true; |
| while (cont) { |
| cont = false; |
| for (Input input : inputs) { |
| cont |= input.tail(); |
| } |
| if (cont) { |
| WatchKey key = watchService.take(); |
| key.pollEvents(); |
| key.reset(); |
| } |
| } |
| } catch (InterruptedException e) { |
| // Ignore, this is the only way to quit |
| } finally { |
| for (Input input : inputs) { |
| input.close(); |
| } |
| } |
| } |
| |
| protected void clear(CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "clear - clear screen", |
| "Usage: clear [OPTIONS]", |
| " -? --help Show help", |
| }; |
| @SuppressWarnings("unused") |
| Options opt = parseOptions(session, usage, argv); |
| if (process.isTty(1)) { |
| Shell.getTerminal(session).puts(Capability.clear_screen); |
| Shell.getTerminal(session).flush(); |
| } |
| } |
| |
| protected void tmux(final CommandSession session, Process process, String[] argv) throws Exception { |
| Commands.tmux(Shell.getTerminal(session), |
| process.out(), System.err, |
| () -> session.get(".tmux"), |
| t -> session.put(".tmux", t), |
| c -> startShell(session, c), |
| Arrays.copyOfRange(argv, 1, argv.length)); |
| } |
| |
| private void startShell(CommandSession session, Terminal terminal) { |
| new Thread(() -> runShell(session, terminal), terminal.getName() + " shell").start(); |
| } |
| |
| private void runShell(CommandSession session, Terminal terminal) { |
| InputStream in = terminal.input(); |
| OutputStream out = terminal.output(); |
| CommandSession newSession = processor.createSession(in, out, out); |
| newSession.put(Shell.VAR_TERMINAL, terminal); |
| newSession.put(".tmux", session.get(".tmux")); |
| Context context = new Context() { |
| public String getProperty(String name) { |
| return System.getProperty(name); |
| } |
| public void exit() throws Exception { |
| terminal.close(); |
| } |
| }; |
| try { |
| new Shell(context, processor).gosh(newSession, new String[]{"--login"}); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } finally { |
| try { |
| terminal.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| protected void ttop(final CommandSession session, Process process, String[] argv) throws Exception { |
| TTop.ttop(Shell.getTerminal(session), process.out(), process.err(), argv); |
| } |
| |
| protected void nano(final CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "nano - edit files", |
| "Usage: nano [FILES]", |
| " -? --help Show help", |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| Nano edit = new Nano(Shell.getTerminal(session), session.currentDir()); |
| edit.open(opt.args()); |
| edit.run(); |
| } |
| |
| protected void watch(final CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "watch - watches & refreshes the output of a command", |
| "Usage: watch [OPTIONS] COMMAND", |
| " -? --help Show help", |
| " -n --interval Interval between executions of the command in seconds", |
| " -a --append The output should be appended but not clear the console" |
| }; |
| |
| Options opt = parseOptions(session, usage, argv); |
| |
| List<String> args = opt.args(); |
| if (args.isEmpty()) { |
| throw new IllegalArgumentException("usage: watch COMMAND"); |
| } |
| ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); |
| final Terminal terminal = Shell.getTerminal(session); |
| final CommandProcessor processor = Shell.getProcessor(session); |
| try { |
| int interval = 1; |
| if (opt.isSet("interval")) { |
| interval = opt.getNumber("interval"); |
| if (interval < 1) { |
| interval = 1; |
| } |
| } |
| final String cmd = String.join(" ", args); |
| Runnable task = () -> { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| PrintStream os = new PrintStream(baos); |
| InputStream is = new ByteArrayInputStream(new byte[0]); |
| if (opt.isSet("append") || !terminal.puts(Capability.clear_screen)) { |
| terminal.writer().println(); |
| } |
| try { |
| CommandSession ns = processor.createSession(is, os, os); |
| Set<String> vars = Shell.getCommands(session); |
| for (String n : vars) { |
| ns.put(n, session.get(n)); |
| } |
| ns.execute(cmd); |
| } catch (Throwable t) { |
| t.printStackTrace(os); |
| } |
| os.flush(); |
| terminal.writer().print(baos.toString()); |
| terminal.writer().flush(); |
| }; |
| executorService.scheduleAtFixedRate(task, 0, interval, TimeUnit.SECONDS); |
| Attributes attr = terminal.enterRawMode(); |
| terminal.reader().read(); |
| terminal.setAttributes(attr); |
| } finally { |
| executorService.shutdownNow(); |
| } |
| } |
| |
| protected void less(CommandSession session, Process process, String[] argv) throws Exception { |
| String[] usage = { |
| "less - file pager", |
| "Usage: less [OPTIONS] [FILES]", |
| " -? --help Show help", |
| " -e --quit-at-eof Exit on second EOF", |
| " -E --QUIT-AT-EOF Exit on EOF", |
| " -F --quit-if-one-screen Exit if entire file fits on first screen", |
| " -q --quiet --silent Silent mode", |
| " -Q --QUIET --SILENT Completely silent", |
| " -S --chop-long-lines Do not fold long lines", |
| " -i --ignore-case Search ignores lowercase case", |
| " -I --IGNORE-CASE Search ignores all case", |
| " -x --tabs Set tab stops", |
| " -N --LINE-NUMBERS Display line number for each line", |
| " --no-init Disable terminal initialization", |
| " --no-keypad Disable keypad handling" |
| }; |
| boolean hasExtendedOptions = false; |
| try { |
| Less.class.getField("quitIfOneScreen"); |
| hasExtendedOptions = true; |
| } catch (NoSuchFieldException e) { |
| List<String> ustrs = new ArrayList<>(Arrays.asList(usage)); |
| ustrs.removeIf(s -> s.contains("--quit-if-one-screen") || s.contains("--no-init") || s.contains("--no-keypad")); |
| usage = ustrs.toArray(new String[ustrs.size()]); |
| } |
| Options opt = parseOptions(session, 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 StdInSource(process)); |
| } else { |
| sources.add(new PathSource(session.currentDir().resolve(arg), arg)); |
| } |
| } |
| |
| if (!process.isTty(1)) { |
| for (Source source : sources) { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(source.read()))) { |
| cat(process, reader, opt.isSet("LINE-NUMBERS")); |
| } |
| } |
| return; |
| } |
| |
| Less less = new Less(Shell.getTerminal(session), null); |
| less.quitAtFirstEof = opt.isSet("QUIT-AT-EOF"); |
| less.quitAtSecondEof = opt.isSet("quit-at-eof"); |
| less.quiet = opt.isSet("quiet"); |
| less.veryQuiet = opt.isSet("QUIET"); |
| less.chopLongLines = opt.isSet("chop-long-lines"); |
| less.ignoreCaseAlways = opt.isSet("IGNORE-CASE"); |
| less.ignoreCaseCond = opt.isSet("ignore-case"); |
| if (opt.isSet("tabs")) { |
| less.tabs(Collections.singletonList(opt.getNumber("tabs"))); |
| } |
| less.printLineNumbers = opt.isSet("LINE-NUMBERS"); |
| if (hasExtendedOptions) { |
| Less.class.getField("quitIfOneScreen").set(less, opt.isSet("quit-if-one-screen")); |
| Less.class.getField("noInit").set(less, opt.isSet("no-init")); |
| Less.class.getField("noKeypad").set(less, opt.isSet("no-keypad")); |
| } |
| less.run(sources); |
| } |
| |
| protected void sort(CommandSession session, Process process, String[] 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(session, usage, argv); |
| |
| List<String> args = opt.args(); |
| |
| List<String> lines = new ArrayList<>(); |
| if (!args.isEmpty()) { |
| for (String filename : args) { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader( |
| session.currentDir().toUri().resolve(filename).toURL().openStream()))) { |
| read(reader, lines); |
| } |
| } |
| } else { |
| BufferedReader r = new BufferedReader(new InputStreamReader(process.in())); |
| read(r, lines); |
| } |
| |
| 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)) { |
| process.out().println(s); |
| } |
| last = s; |
| } |
| } |
| |
| protected void pwd(CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "pwd - get current directory", |
| "Usage: pwd [OPTIONS]", |
| " -? --help show help" |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| if (!opt.args().isEmpty()) { |
| throw new IllegalArgumentException("usage: pwd"); |
| } |
| process.out().println(session.currentDir()); |
| } |
| |
| protected void cd(CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "cd - get current directory", |
| "Usage: cd [OPTIONS] DIRECTORY", |
| " -? --help show help" |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| if (opt.args().size() != 1) { |
| throw new IllegalArgumentException("usage: cd DIRECTORY"); |
| } |
| Path cwd = session.currentDir(); |
| cwd = cwd.resolve(opt.args().get(0)).toAbsolutePath().normalize(); |
| if (!Files.exists(cwd)) { |
| throw new IOException("no such file or directory: " + opt.args().get(0)); |
| } else if (!Files.isDirectory(cwd)) { |
| throw new IOException("not a directory: " + opt.args().get(0)); |
| } |
| session.currentDir(cwd); |
| } |
| |
| protected void ls(CommandSession session, Process process, 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(session, usage, argv); |
| 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 = process.isTty(1); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’"); |
| } |
| Map<String, String> colors = colored ? getLsColorMap(session) : 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; |
| this.path = abs.startsWith(root) ? root.relativize(abs) : abs; |
| 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.toFile())); |
| return attrs; |
| } |
| } |
| |
| Path currentDir = session.currentDir(); |
| // Listing |
| List<Path> expanded = new ArrayList<>(); |
| if (opt.args().isEmpty()) { |
| expanded.add(currentDir); |
| } else { |
| opt.args().forEach(s -> expanded.add(currentDir.resolve(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 = process.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 (process.isTty(1)) { |
| 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(session, process, 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() + ":"); |
| } |
| display.accept(Stream.concat(Stream.of(".", "..").map(path::resolve), Files.list(path)) |
| .filter(filter) |
| .map(p -> new PathEntry(p, path)) |
| .sorted() |
| ); |
| } |
| } |
| |
| private void toColumn(CommandSession session, Process process, PrintStream out, Stream<String> ansi, boolean horizontal) { |
| Terminal terminal = Shell.getTerminal(session); |
| int width = process.isTty(1) ? 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)); |
| } |
| } |
| |
| protected void cat(CommandSession session, Process process, 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(session, usage, argv); |
| List<String> args = opt.args(); |
| if (args.isEmpty()) { |
| args = Collections.singletonList("-"); |
| } |
| Path cwd = session.currentDir(); |
| for (String arg : args) { |
| InputStream is; |
| if ("-".equals(arg)) { |
| is = process.in(); |
| } else { |
| is = cwd.toUri().resolve(arg).toURL().openStream(); |
| } |
| cat(process, new BufferedReader(new InputStreamReader(is)), opt.isSet("n")); |
| } |
| } |
| |
| protected void echo(CommandSession session, Process process, Object[] argv) throws Exception { |
| final String[] usage = { |
| "echo - echoes or prints ARGUMENT to standard output", |
| "Usage: echo [OPTIONS] [ARGUMENTS]", |
| " -? --help show help", |
| " -n no trailing new line" |
| }; |
| Options opt = parseOptions(session, usage, argv); |
| List<String> args = opt.args(); |
| StringBuilder buf = new StringBuilder(); |
| if (args != null) { |
| for (String arg : args) { |
| if (buf.length() > 0) |
| buf.append(' '); |
| for (int i = 0; i < arg.length(); i++) { |
| int c = arg.charAt(i); |
| int ch; |
| if (c == '\\') { |
| c = i < arg.length() - 1 ? arg.charAt(++i) : '\\'; |
| switch (c) { |
| case 'a': |
| buf.append('\u0007'); |
| break; |
| case 'n': |
| buf.append('\n'); |
| break; |
| case 't': |
| buf.append('\t'); |
| break; |
| case 'r': |
| buf.append('\r'); |
| break; |
| case '\\': |
| buf.append('\\'); |
| break; |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| ch = 0; |
| for (int j = 0; j < 3; j++) { |
| c = i < arg.length() - 1 ? arg.charAt(++i) : -1; |
| if (c >= 0) { |
| ch = ch * 8 + (c - '0'); |
| } |
| } |
| buf.append((char) ch); |
| break; |
| case 'u': |
| ch = 0; |
| for (int j = 0; j < 4; j++) { |
| c = i < arg.length() - 1 ? arg.charAt(++i) : -1; |
| if (c >= 0) { |
| if (c >= 'A' && c <= 'Z') { |
| ch = ch * 16 + (c - 'A' + 10); |
| } else if (c >= 'a' && c <= 'z') { |
| ch = ch * 16 + (c - 'a' + 10); |
| } else if (c >= '0' && c <= '9') { |
| ch = ch * 16 + (c - '0'); |
| } else { |
| break; |
| } |
| } |
| } |
| buf.append((char) ch); |
| break; |
| default: |
| buf.append((char) c); |
| break; |
| } |
| } else { |
| buf.append((char) c); |
| } |
| } |
| } |
| } |
| if (opt.isSet("n")) { |
| process.out().print(buf); |
| } else { |
| process.out().println(buf); |
| } |
| } |
| |
| protected void grep(CommandSession session, Process process, 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(session, usage, argv); |
| 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 context = opt.isSet("context") ? opt.getNumber("context") : 0; |
| String lineFmt = opt.isSet("pad-lines") ? "%6d" : "%d"; |
| if (after < 0) { |
| after = context; |
| } |
| if (before < 0) { |
| before = context; |
| } |
| List<String> lines = new ArrayList<>(); |
| boolean invertMatch = opt.isSet("invert-match"); |
| boolean lineNumber = opt.isSet("line-number"); |
| boolean count = opt.isSet("count"); |
| 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 = process.isTty(1); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’"); |
| } |
| Map<String, String> colors = colored ? getColorMap(session, "GREP", DEFAULT_GREP_COLORS) : Collections.emptyMap(); |
| |
| List<Source> sources = new ArrayList<>(); |
| if (opt.args().isEmpty()) { |
| opt.args().add("-"); |
| } |
| for (String arg : opt.args()) { |
| if ("-".equals(arg)) { |
| sources.add(new StdInSource(process)); |
| } else { |
| sources.add(new PathSource(session.currentDir().resolve(arg), arg)); |
| } |
| } |
| boolean match = false; |
| for (Source source : sources) { |
| boolean firstPrint = true; |
| int nb = 0; |
| int lineno = 1; |
| String line; |
| int lineMatch = 0; |
| try (BufferedReader r = new BufferedReader(new InputStreamReader(source.read()))) { |
| while ((line = r.readLine()) != null) { |
| if (line.length() == 1 && line.charAt(0) == '\n') { |
| break; |
| } |
| boolean matches = p.matcher(line).matches(); |
| AttributedStringBuilder sbl = new AttributedStringBuilder(); |
| if (!count) { |
| if (sources.size() > 1) { |
| if (colored) { |
| applyStyle(sbl, colors, "fn"); |
| } |
| sbl.append(source.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((matches ^ invertMatch) ? ":" : "-"); |
| } |
| String style = matches ^ invertMatch ^ (invertMatch && colors.containsKey("rv")) |
| ? "sl" : "cx"; |
| if (colored) { |
| applyStyle(sbl, colors, style); |
| } |
| AttributedString aLine = AttributedString.fromAnsi(line); |
| Matcher matcher2 = p2.matcher(aLine.toString()); |
| int cur = 0; |
| while (matcher2.find()) { |
| int index = matcher2.start(0); |
| AttributedString prefix = aLine.subSequence(cur, index); |
| sbl.append(prefix); |
| cur = matcher2.end(); |
| if (colored) { |
| applyStyle(sbl, colors, invertMatch ? "mc" : "ms", "mt"); |
| } |
| sbl.append(aLine.subSequence(index, cur)); |
| if (colored) { |
| applyStyle(sbl, colors, style); |
| } |
| nb++; |
| } |
| sbl.append(aLine.subSequence(cur, aLine.length())); |
| } |
| if (matches ^ invertMatch) { |
| lines.add(sbl.toAnsi(Shell.getTerminal(session))); |
| lineMatch = lines.size(); |
| } else { |
| if (lineMatch != 0 & lineMatch + after + before <= lines.size()) { |
| if (!count) { |
| if (!firstPrint && before + after > 0) { |
| AttributedStringBuilder sbl2 = new AttributedStringBuilder(); |
| if (colored) { |
| applyStyle(sbl2, colors, "se"); |
| } |
| sbl2.append("--"); |
| process.out().println(sbl2.toAnsi(Shell.getTerminal(session))); |
| } else { |
| firstPrint = false; |
| } |
| for (int i = 0; i < lineMatch + after; i++) { |
| process.out().println(lines.get(i)); |
| } |
| } |
| while (lines.size() > before) { |
| lines.remove(0); |
| } |
| lineMatch = 0; |
| } |
| lines.add(sbl.toAnsi(Shell.getTerminal(session))); |
| 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("--"); |
| process.out().println(sbl2.toAnsi(Shell.getTerminal(session))); |
| } else { |
| firstPrint = false; |
| } |
| for (int i = 0; i < lineMatch + after && i < lines.size(); i++) { |
| process.out().println(lines.get(i)); |
| } |
| } |
| if (count) { |
| process.out().println(nb); |
| } |
| match |= nb > 0; |
| } |
| } |
| Process.Utils.current().error(match ? 0 : 1); |
| } |
| |
| protected void sleep(CommandSession session, Process process, String[] argv) throws Exception { |
| final String[] usage = { |
| "sleep - suspend execution for an interval of time", |
| "Usage: sleep seconds", |
| " -? --help show help"}; |
| |
| Options opt = parseOptions(session, usage, argv); |
| List<String> args = opt.args(); |
| if (args.size() != 1) { |
| throw new IllegalArgumentException("usage: sleep seconds"); |
| } else { |
| int s = Integer.parseInt(args.get(0)); |
| Thread.sleep(s * 1000); |
| } |
| } |
| |
| protected static void read(BufferedReader r, List<String> lines) throws IOException { |
| for (String s = r.readLine(); s != null; s = r.readLine()) { |
| lines.add(s); |
| } |
| } |
| |
| private static void cat(Process process, final BufferedReader reader, boolean displayLineNumbers) throws IOException { |
| String line; |
| int lineno = 1; |
| try { |
| while ((line = reader.readLine()) != null) { |
| if (displayLineNumbers) { |
| process.out().print(String.format("%6d ", lineno++)); |
| } |
| process.out().println(line); |
| } |
| } finally { |
| reader.close(); |
| } |
| } |
| |
| public static class SortComparator implements Comparator<String> { |
| |
| private static Pattern fpPattern; |
| |
| static { |
| final String Digits = "(\\p{Digit}+)"; |
| final String HexDigits = "(\\p{XDigit}+)"; |
| final String Exp = "[eE][+-]?" + Digits; |
| final String fpRegex = "([\\x00-\\x20]*[+-]?(NaN|Infinity|(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|(\\.(" + Digits + ")(" + Exp + ")?)|(((0[xX]" + HexDigits + "(\\.)?)|(0[xX]" + HexDigits + "?(\\.)" + HexDigits + "))[pP][+-]?" + Digits + "))" + "[fFdD]?))[\\x00-\\x20]*)(.*)"; |
| fpPattern = Pattern.compile(fpRegex); |
| } |
| |
| private boolean caseInsensitive; |
| private boolean reverse; |
| private boolean ignoreBlanks; |
| private boolean numeric; |
| private char separator; |
| private List<Key> sortKeys; |
| |
| public SortComparator(boolean caseInsensitive, |
| boolean reverse, |
| boolean ignoreBlanks, |
| boolean numeric, |
| char separator, |
| List<String> sortFields) { |
| this.caseInsensitive = caseInsensitive; |
| this.reverse = reverse; |
| this.separator = separator; |
| this.ignoreBlanks = ignoreBlanks; |
| this.numeric = numeric; |
| if (sortFields == null || sortFields.size() == 0) { |
| sortFields = new ArrayList<>(); |
| sortFields.add("1"); |
| } |
| sortKeys = sortFields.stream().map(Key::new).collect(Collectors.toList()); |
| } |
| |
| public int compare(String o1, String o2) { |
| int res = 0; |
| |
| List<Integer> fi1 = getFieldIndexes(o1); |
| List<Integer> fi2 = getFieldIndexes(o2); |
| for (Key key : sortKeys) { |
| int[] k1 = getSortKey(o1, fi1, key); |
| int[] k2 = getSortKey(o2, fi2, key); |
| if (key.numeric) { |
| Double d1 = getDouble(o1, k1[0], k1[1]); |
| Double d2 = getDouble(o2, k2[0], k2[1]); |
| res = d1.compareTo(d2); |
| } else { |
| res = compareRegion(o1, k1[0], k1[1], o2, k2[0], k2[1], key.caseInsensitive); |
| } |
| if (res != 0) { |
| if (key.reverse) { |
| res = -res; |
| } |
| break; |
| } |
| } |
| return res; |
| } |
| |
| protected Double getDouble(String s, int start, int end) { |
| Matcher m = fpPattern.matcher(s.substring(start, end)); |
| m.find(); |
| return new Double(s.substring(0, m.end(1))); |
| } |
| |
| protected int compareRegion(String s1, int start1, int end1, String s2, int start2, int end2, boolean caseInsensitive) { |
| for (int i1 = start1, i2 = start2; i1 < end1 && i2 < end2; i1++, i2++) { |
| char c1 = s1.charAt(i1); |
| char c2 = s2.charAt(i2); |
| if (c1 != c2) { |
| if (caseInsensitive) { |
| c1 = Character.toUpperCase(c1); |
| c2 = Character.toUpperCase(c2); |
| if (c1 != c2) { |
| c1 = Character.toLowerCase(c1); |
| c2 = Character.toLowerCase(c2); |
| if (c1 != c2) { |
| return c1 - c2; |
| } |
| } |
| } else { |
| return c1 - c2; |
| } |
| } |
| } |
| return end1 - end2; |
| } |
| |
| protected int[] getSortKey(String str, List<Integer> fields, Key key) { |
| int start; |
| int end; |
| if (key.startField * 2 <= fields.size()) { |
| start = fields.get((key.startField - 1) * 2); |
| if (key.ignoreBlanksStart) { |
| while (start < fields.get((key.startField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(start))) { |
| start++; |
| } |
| } |
| if (key.startChar > 0) { |
| start = Math.min(start + key.startChar - 1, fields.get((key.startField - 1) * 2 + 1)); |
| } |
| } else { |
| start = 0; |
| } |
| if (key.endField > 0 && key.endField * 2 <= fields.size()) { |
| end = fields.get((key.endField - 1) * 2); |
| if (key.ignoreBlanksEnd) { |
| while (end < fields.get((key.endField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(end))) { |
| end++; |
| } |
| } |
| if (key.endChar > 0) { |
| end = Math.min(end + key.endChar - 1, fields.get((key.endField - 1) * 2 + 1)); |
| } |
| } else { |
| end = str.length(); |
| } |
| return new int[]{start, end}; |
| } |
| |
| protected List<Integer> getFieldIndexes(String o) { |
| List<Integer> fields = new ArrayList<>(); |
| if (o.length() > 0) { |
| if (separator == '\0') { |
| fields.add(0); |
| for (int idx = 1; idx < o.length(); idx++) { |
| if (Character.isWhitespace(o.charAt(idx)) && !Character.isWhitespace(o.charAt(idx - 1))) { |
| fields.add(idx - 1); |
| fields.add(idx); |
| } |
| } |
| fields.add(o.length() - 1); |
| } else { |
| int last = -1; |
| for (int idx = o.indexOf(separator); idx >= 0; idx = o.indexOf(separator, idx + 1)) { |
| if (last >= 0) { |
| fields.add(last); |
| fields.add(idx - 1); |
| } else if (idx > 0) { |
| fields.add(0); |
| fields.add(idx - 1); |
| } |
| last = idx + 1; |
| } |
| if (last < o.length()) { |
| fields.add(last < 0 ? 0 : last); |
| fields.add(o.length() - 1); |
| } |
| } |
| } |
| return fields; |
| } |
| |
| public class Key { |
| int startField; |
| int startChar; |
| int endField; |
| int endChar; |
| boolean ignoreBlanksStart; |
| boolean ignoreBlanksEnd; |
| boolean caseInsensitive; |
| boolean reverse; |
| boolean numeric; |
| |
| public Key(String str) { |
| boolean modifiers = false; |
| boolean startPart = true; |
| boolean inField = true; |
| boolean inChar = false; |
| for (char c : str.toCharArray()) { |
| switch (c) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| if (!inField && !inChar) { |
| throw new IllegalArgumentException("Bad field syntax: " + str); |
| } |
| if (startPart) { |
| if (inChar) { |
| startChar = startChar * 10 + (c - '0'); |
| } else { |
| startField = startField * 10 + (c - '0'); |
| } |
| } else { |
| if (inChar) { |
| endChar = endChar * 10 + (c - '0'); |
| } else { |
| endField = endField * 10 + (c - '0'); |
| } |
| } |
| break; |
| case '.': |
| if (!inField) { |
| throw new IllegalArgumentException("Bad field syntax: " + str); |
| } |
| inField = false; |
| inChar = true; |
| break; |
| case 'n': |
| inField = false; |
| inChar = false; |
| modifiers = true; |
| numeric = true; |
| break; |
| case 'f': |
| inField = false; |
| inChar = false; |
| modifiers = true; |
| caseInsensitive = true; |
| break; |
| case 'r': |
| inField = false; |
| inChar = false; |
| modifiers = true; |
| reverse = true; |
| break; |
| case 'b': |
| inField = false; |
| inChar = false; |
| modifiers = true; |
| if (startPart) { |
| ignoreBlanksStart = true; |
| } else { |
| ignoreBlanksEnd = true; |
| } |
| break; |
| case ',': |
| inField = true; |
| inChar = false; |
| startPart = false; |
| break; |
| default: |
| throw new IllegalArgumentException("Bad field syntax: " + str); |
| } |
| } |
| if (!modifiers) { |
| ignoreBlanksStart = ignoreBlanksEnd = SortComparator.this.ignoreBlanks; |
| reverse = SortComparator.this.reverse; |
| caseInsensitive = SortComparator.this.caseInsensitive; |
| numeric = SortComparator.this.numeric; |
| } |
| if (startField < 1) { |
| throw new IllegalArgumentException("Bad field syntax: " + str); |
| } |
| } |
| } |
| } |
| |
| 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(); |
| } |
| } |
| |
| /** |
| * @param fileName The file name to be evaluated - ignored if {@code null}/empty |
| * @return {@code true} if the file ends in one of the {@link #WINDOWS_EXECUTABLE_EXTENSIONS} |
| */ |
| 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; |
| } |
| |
| /** |
| * @param f The {@link File} to be checked |
| * @return A {@link Set} of {@link PosixFilePermission}s based on whether |
| * the file is readable/writable/executable. If so, then <U>all</U> the |
| * relevant permissions are set (i.e., owner, group and others) |
| */ |
| private static Set<PosixFilePermission> getPermissionsFromFile(File f) { |
| Set<PosixFilePermission> perms = EnumSet.noneOf(PosixFilePermission.class); |
| if (f.canRead()) { |
| perms.add(PosixFilePermission.OWNER_READ); |
| perms.add(PosixFilePermission.GROUP_READ); |
| perms.add(PosixFilePermission.OTHERS_READ); |
| } |
| |
| if (f.canWrite()) { |
| perms.add(PosixFilePermission.OWNER_WRITE); |
| perms.add(PosixFilePermission.GROUP_WRITE); |
| perms.add(PosixFilePermission.OTHERS_WRITE); |
| } |
| |
| if (f.canExecute() || (OSUtils.IS_WINDOWS && isWindowsExecutable(f.getName()))) { |
| perms.add(PosixFilePermission.OWNER_EXECUTE); |
| perms.add(PosixFilePermission.GROUP_EXECUTE); |
| perms.add(PosixFilePermission.OTHERS_EXECUTE); |
| } |
| |
| return perms; |
| } |
| |
| public static Map<String, String> getLsColorMap(CommandSession session) { |
| return getColorMap(session, "LS", DEFAULT_LS_COLORS); |
| } |
| |
| public static Map<String, String> getColorMap(CommandSession session, String name, String def) { |
| Object obj = session.get(name + "_COLORS"); |
| String str = obj != null ? obj.toString() : null; |
| if (str == null) { |
| str = def; |
| } |
| String sep = str.matches("[a-z]{2}=[0-9]*(;[0-9]+)*(:[a-z]{2}=[0-9]*(;[0-9]+)*)*") ? ":" : " "; |
| return Arrays.stream(str.split(sep)) |
| .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), |
| s -> s.substring(s.indexOf('=') + 1))); |
| } |
| |
| static String applyStyle(String text, Map<String, String> colors, String... types) { |
| String t = null; |
| for (String type : types) { |
| if (colors.get(type) != null) { |
| t = type; |
| break; |
| } |
| } |
| return new AttributedString(text, new StyleResolver(colors::get).resolve("." + t)) |
| .toAnsi(); |
| } |
| |
| static void applyStyle(AttributedStringBuilder sb, Map<String, String> colors, String... types) { |
| String t = null; |
| for (String type : types) { |
| if (colors.get(type) != null) { |
| t = type; |
| break; |
| } |
| } |
| sb.style(new StyleResolver(colors::get).resolve("." + t)); |
| } |
| |
| private static class StdInSource implements Source { |
| |
| private final Process process; |
| |
| StdInSource(Process process) { |
| this.process = process; |
| } |
| |
| @Override |
| public String getName() { |
| return null; |
| } |
| |
| @Override |
| public InputStream read() { |
| return process.in(); |
| } |
| |
| @Override |
| public Long lines() { |
| return null; |
| } |
| } |
| } |