diff --git a/builtins/src/main/java/org/jline/builtins/Commands.java b/builtins/src/main/java/org/jline/builtins/Commands.java index 893b734cf..18fdde7ee 100644 --- a/builtins/src/main/java/org/jline/builtins/Commands.java +++ b/builtins/src/main/java/org/jline/builtins/Commands.java @@ -8,6 +8,10 @@ */ package org.jline.builtins; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; @@ -186,8 +190,9 @@ public static void history(LineReader reader, PrintStream out, PrintStream err, String[] argv) throws Exception { final String[] usage = { "history - list history of commands", - "Usage: history [-dnrfEi] [-m match] [first] [last]", + "Usage: history [-dnrfEie] [-m match] [first] [last]", " history -ARWI [filename]", + " history -s [old=new] [command]", " history --clear", " history --save", " -? --help Displays command help", @@ -209,7 +214,9 @@ public static void history(LineReader reader, PrintStream out, PrintStream err, " to the file are added", " [first] [last] These optional arguments may be specified as a number or as a string. A negative number", " is used as an offset to the current history event number. A string specifies the most", - " recent event beginning with the given string."}; + " recent event beginning with the given string.", + " -e Uses the nano editor to edit the commands before executing", + " -s Re-executes the command without invoking an editor"}; Options opt = Options.compile(usage).parse(argv); if (opt.isSet("help")) { @@ -237,9 +244,11 @@ public static void history(LineReader reader, PrintStream out, PrintStream err, if (done) { return; } - int argId = 0; + ReExecute execute = new ReExecute(history, opt); + int argId = execute.getArgId(); + Pattern pattern = null; - if (opt.isSet("m") && opt.args().size() > 0) { + if (opt.isSet("m") && opt.args().size() > argId) { StringBuilder sb = new StringBuilder(); char prev = '0'; for (char c: opt.args().get(argId++).toCharArray()) { @@ -251,11 +260,11 @@ public static void history(LineReader reader, PrintStream out, PrintStream err, } pattern = Pattern.compile(sb.toString(), Pattern.DOTALL); } + boolean reverse = opt.isSet("r") || (opt.isSet("s") && opt.args().size() <= argId); int firstId = opt.args().size() > argId ? retrieveHistoryId(history, opt.args().get(argId++)) : -17; int lastId = opt.args().size() > argId ? retrieveHistoryId(history, opt.args().get(argId++)) : -1; firstId = historyId(firstId, history.first(), history.last()); lastId = historyId(lastId, history.first(), history.last()); - boolean reverse = opt.isSet("r"); if (firstId > lastId) { int tmpId = firstId; firstId = lastId; @@ -271,41 +280,132 @@ public static void history(LineReader reader, PrintStream out, PrintStream err, } else { iter = history.iterator(firstId); } + while (iter.hasNext() && listed < tot) { History.Entry entry = iter.next(); listed++; if (pattern != null && !pattern.matcher(entry.line()).matches()) { continue; } - AttributedStringBuilder sb = new AttributedStringBuilder(); - if (!opt.isSet("n")) { - sb.append(" "); - sb.styled(AttributedStyle::bold, String.format("%3d", entry.index())); - } - if (opt.isSet("d") || opt.isSet("f") || opt.isSet("E") || opt.isSet("i")) { - sb.append(" "); - if (opt.isSet("d")) { - LocalTime lt = LocalTime.from(entry.time().atZone(ZoneId.systemDefault())) - .truncatedTo(ChronoUnit.SECONDS); - DateTimeFormatter.ISO_LOCAL_TIME.formatTo(lt, sb); + if (execute.isExecute()) { + if (execute.isEdit()) { + execute.addCommandInFile(entry.line()); } else { - LocalDateTime lt = LocalDateTime.from(entry.time().atZone(ZoneId.systemDefault()) - .truncatedTo(ChronoUnit.MINUTES)); - String format = "yyyy-MM-dd hh:mm"; - if (opt.isSet("f")) { - format = "MM/dd/yy hh:mm"; - } else if (opt.isSet("E")) { - format = "dd.MM.yyyy hh:mm"; + execute.addCommandInBuffer(reader, entry.line()); + break; + } + } else { + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (!opt.isSet("n")) { + sb.append(" "); + sb.styled(AttributedStyle::bold, String.format("%3d", entry.index())); + } + if (opt.isSet("d") || opt.isSet("f") || opt.isSet("E") || opt.isSet("i")) { + sb.append(" "); + if (opt.isSet("d")) { + LocalTime lt = LocalTime.from(entry.time().atZone(ZoneId.systemDefault())) + .truncatedTo(ChronoUnit.SECONDS); + DateTimeFormatter.ISO_LOCAL_TIME.formatTo(lt, sb); + } else { + LocalDateTime lt = LocalDateTime.from(entry.time().atZone(ZoneId.systemDefault()) + .truncatedTo(ChronoUnit.MINUTES)); + String format = "yyyy-MM-dd hh:mm"; + if (opt.isSet("f")) { + format = "MM/dd/yy hh:mm"; + } else if (opt.isSet("E")) { + format = "dd.MM.yyyy hh:mm"; + } + DateTimeFormatter.ofPattern(format).formatTo(lt, sb); } - DateTimeFormatter.ofPattern(format).formatTo(lt, sb); } + sb.append(" "); + sb.append(highlighter.highlight(reader, entry.line())); + out.println(sb.toAnsi(reader.getTerminal())); } - sb.append(" "); - sb.append(highlighter.highlight(reader, entry.line())); - out.println(sb.toAnsi(reader.getTerminal())); } + execute.editCommandsAndClose(reader); } + private static class ReExecute { + private final boolean execute; + private final boolean edit; + private String oldParam; + private String newParam; + private FileWriter cmdWriter; + private File cmdFile; + private int argId = 0; + + public ReExecute(History history, Options opt) throws IOException { + execute = opt.isSet("e") || opt.isSet("s"); + edit = opt.isSet("e"); + if (execute) { + Iterator iter = history.reverseIterator(history.last()); + if (iter.hasNext()) { + iter.next(); + iter.remove(); + } + if (edit) { + cmdFile = File.createTempFile("jline-history-", null); + cmdWriter = new FileWriter(cmdFile); + } else if (opt.args().size() > 0 ) { + String[] s = opt.args().get(argId).split("="); + if (s.length == 2) { + argId = argId + 1; + oldParam = s[0]; + newParam = s[1]; + } + } + } + } + + public int getArgId() { + return argId; + } + + public boolean isEdit() { + return edit; + } + + public boolean isExecute() { + return execute; + } + + public void addCommandInFile(String command) throws IOException { + cmdWriter.write(command + "\n"); + } + + public void addCommandInBuffer(LineReader reader, String command) { + reader.addCommandsInBuffer(Arrays.asList(replaceParam(command))); + } + + private String replaceParam(String command) { + String out = command; + if (oldParam != null && newParam != null) { + out = command.replaceAll(oldParam, newParam); + } + return out; + } + + public void editCommandsAndClose(LineReader reader) throws IOException { + if (edit) { + cmdWriter.close(); + Nano editor = new Nano(reader.getTerminal(), new File(cmdFile.getParent())); + editor.setRestricted(true); + editor.open(Arrays.asList(cmdFile.getName())); + editor.run(); + List commands = new ArrayList<>(); + BufferedReader br = new BufferedReader(new FileReader(cmdFile)); + String line; + while ((line = br.readLine()) != null) { + commands.add(line); + } + br.close(); + reader.addCommandsInBuffer(commands); + cmdFile.delete(); + } + } + } + private static int historyId(int id, int minId, int maxId) { int out = id; if (id < 0) { diff --git a/builtins/src/main/java/org/jline/builtins/Nano.java b/builtins/src/main/java/org/jline/builtins/Nano.java index 8fccfb1ab..957210284 100644 --- a/builtins/src/main/java/org/jline/builtins/Nano.java +++ b/builtins/src/main/java/org/jline/builtins/Nano.java @@ -73,7 +73,6 @@ public class Nano { protected final BindingReader bindingReader; protected final Size size; protected final Path root; - protected final boolean restricted; protected final int vsusp; // Keys @@ -109,6 +108,7 @@ public class Nano { protected boolean searchCaseSensitive; protected boolean searchRegexp; protected boolean searchBackwards; + protected boolean restricted; protected String searchTerm; protected List searchTerms = new ArrayList<>(); protected int searchTermId = -1; @@ -961,6 +961,10 @@ public Nano(Terminal terminal, Path root, Options opts) { bindKeys(); } + public void setRestricted(boolean restricted) { + this.restricted = restricted; + } + public void open(String... files) throws IOException { open(Arrays.asList(files)); } diff --git a/builtins/src/test/java/org/jline/example/Example.java b/builtins/src/test/java/org/jline/example/Example.java index 1ada9965d..2ceb52dd6 100644 --- a/builtins/src/test/java/org/jline/example/Example.java +++ b/builtins/src/test/java/org/jline/example/Example.java @@ -340,38 +340,28 @@ public void complete(LineReader reader, ParsedLine line, List candida String line = null; try { line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); - } catch (UserInterruptException e) { - // Ignore - } catch (EndOfFileException e) { - return; - } - if (line == null) { - continue; - } - - line = line.trim(); + line = line.trim(); - if (color) { - terminal.writer().println( + if (color) { + terminal.writer().println( AttributedString.fromAnsi("\u001B[33m======>\u001B[0m\"" + line + "\"") .toAnsi(terminal)); - } else { - terminal.writer().println("======>\"" + line + "\""); - } - terminal.flush(); + } else { + terminal.writer().println("======>\"" + line + "\""); + } + terminal.flush(); - // If we input the special word then we will mask - // the next line. - if ((trigger != null) && (line.compareTo(trigger) == 0)) { - line = reader.readLine("password> ", mask); - } - if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) { - break; - } - ParsedLine pl = reader.getParser().parse(line, 0); - String[] argv = pl.words().subList(1, pl.words().size()).toArray(new String[0]); - try { + // If we input the special word then we will mask + // the next line. + if ((trigger != null) && (line.compareTo(trigger) == 0)) { + line = reader.readLine("password> ", mask); + } + if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) { + break; + } + ParsedLine pl = reader.getParser().parse(line, 0); + String[] argv = pl.words().subList(1, pl.words().size()).toArray(new String[0]); if ("set".equals(pl.word())) { if (pl.words().size() == 3) { reader.setVariable(pl.words().get(1), pl.words().get(2)); @@ -410,9 +400,6 @@ else if ("cls".equals(pl.word())) { else if ("sleep".equals(pl.word())) { Thread.sleep(3000); } - // - // builtin commands are added in order to test HelpPrinter class - // else if ("tmux".equals(pl.word())) { Commands.tmux(terminal, System.out, System.err, null, //Supplier getter, @@ -465,6 +452,12 @@ else if ("help".equals(pl.word()) || "?".equals(pl.word())) { catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } + catch (UserInterruptException e) { + // Ignore + } + catch (EndOfFileException e) { + return; + } } } catch (Throwable t) { diff --git a/reader/src/main/java/org/jline/reader/History.java b/reader/src/main/java/org/jline/reader/History.java index 274d633ba..f52526eb6 100644 --- a/reader/src/main/java/org/jline/reader/History.java +++ b/reader/src/main/java/org/jline/reader/History.java @@ -133,6 +133,11 @@ public boolean hasNext() { public Entry next() { return it.previous(); } + @Override + public void remove() { + it.remove(); + resetIndex(); + } }; } @@ -191,4 +196,9 @@ public Entry next() { * all of the other iterator. */ void moveToEnd(); + + /** + * Reset index after remove + */ + void resetIndex(); } diff --git a/reader/src/main/java/org/jline/reader/LineReader.java b/reader/src/main/java/org/jline/reader/LineReader.java index 7c32998c4..ec3e85f1d 100644 --- a/reader/src/main/java/org/jline/reader/LineReader.java +++ b/reader/src/main/java/org/jline/reader/LineReader.java @@ -9,6 +9,7 @@ package org.jline.reader; import java.io.InputStream; +import java.util.Collection; import java.util.Map; import java.util.function.IntConsumer; @@ -655,4 +656,6 @@ enum RegionType { int getRegionMark(); + void addCommandsInBuffer(Collection commands); + } diff --git a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java index b36427601..736a5742d 100644 --- a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java +++ b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -253,7 +253,11 @@ protected enum BellType { protected boolean nextCommandFromHistory = false; protected int nextHistoryId = -1; - + /* + * execute commands from history + */ + protected List commandsBuffer = new ArrayList<>(); + public LineReaderImpl(Terminal terminal) throws IOException { this(terminal, null, null); } @@ -471,7 +475,29 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin // prompt may be null // maskingCallback may be null // buffer may be null - + if (!commandsBuffer.isEmpty()) { + String cmd = commandsBuffer.remove(0); + boolean done = false; + do { + try { + parser.parse(cmd, cmd.length() + 1, ParseContext.ACCEPT_LINE); + done = true; + } catch (EOFError e) { + if (commandsBuffer.isEmpty()) { + throw new IllegalArgumentException("Incompleted command: \n" + cmd); + } + cmd += "\n"; + cmd += commandsBuffer.remove(0); + } catch (SyntaxError e) { + done = true; + } + } while (!done); + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.styled(AttributedStyle::bold, cmd); + sb.toAttributedString().println(terminal); + terminal.flush(); + return finish(cmd); + } if (!startedReading.compareAndSet(false, true)) { throw new IllegalStateException(); } @@ -983,6 +1009,10 @@ public void unsetOpt(Option option) { options.put(option, Boolean.FALSE); } + @Override + public void addCommandsInBuffer(Collection commands) { + commandsBuffer.addAll(commands); + } // @@ -995,7 +1025,10 @@ public void unsetOpt(Option option) { * @return the former contents of the buffer. */ protected String finishBuffer() { - String str = buf.toString(); + return finish(buf.toString()); + } + + protected String finish(String str) { String historyLine = str; if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { diff --git a/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java b/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java index 45b211881..1b1f0a0c8 100644 --- a/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java +++ b/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java @@ -435,6 +435,10 @@ public ListIterator iterator(int index) { public Spliterator spliterator() { return items.spliterator(); } + + public void resetIndex() { + index = index > items.size() ? items.size() : index; + } protected static class EntryImpl implements Entry {