Skip to content

Commit

Permalink
Introduce MaskingCallback to provide hooks to customize line output, f…
Browse files Browse the repository at this point in the history
…ixes #163

Patch provided by John Poth, thx !
  • Loading branch information
gnodet committed Sep 5, 2017
1 parent 15df62e commit 3793dcd
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2002-2017, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.example;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;

public class ArgumentMaskCallbackReader
{
/**
* Mask a certain argument for a given command in the console. So for example 'add-user 2', 'add-user username password'
* will be displayed as 'add-user username ********'.
*/
public static void usage() {
System.out.println("Usage: java "
+ ArgumentMaskCallbackReader.class.getName() + " [command] [argument-pos-to-mask]");
}

public static void main(String[] args) throws IOException {
Terminal terminal = TerminalBuilder.terminal();
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal).build();

String command = args[0];
int pos = Integer.valueOf(args[1]);

MaskingCallback maskingCallback = new CommandArgumentMask(command, pos);

String line;
do {
line = reader.readLine("prompt> ", null, maskingCallback, null);
System.out.println("Got line: " + line);
}
while (line != null && line.length() > 0);
}

private static class CommandArgumentMask implements MaskingCallback {

private final Pattern pattern;
private static final Character mask = '*';
private final int pos;

public CommandArgumentMask(String command, int pos) {
StringBuilder regex = new StringBuilder();
regex.append(Pattern.quote(command));
for (int i = 0; i < pos; i++) {
regex.append(" +([^ ]+)");
}
this.pattern = Pattern.compile(regex.toString());
this.pos = pos;
}

@Override
public String display(String line) {
return filter(line);
}

@Override
public String history(String line)
{
final String filter = filter(line);
System.out.println();
System.out.print("Adding to history: " + filter );
return filter;
}

public String filter(String line) {
Matcher m = pattern.matcher(line);

if( m.find() ) {
StringBuilder maskedLine = new StringBuilder(line);
for(int i = m.start(pos); i < m.end(pos); i++){
maskedLine.replace(i,i+1, String.valueOf(mask));
}
return maskedLine.toString();
} else {
return line;
}
}
}
}
2 changes: 1 addition & 1 deletion builtins/src/test/java/org/jline/example/Example.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public void complete(LineReader reader, ParsedLine line, List<Candidate> candida
while (true) {
String line = null;
try {
line = reader.readLine(prompt, rightPrompt, null, null);
line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2002-2017, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.example;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;

public class OptionMaskCallbackReader
{
/**
* Mask a certain option value in the command line. So for example for the option '--password', '--password mypassword'
* will be displayed as '--password **********'.
*/
public static void usage() {
System.out.println("Usage: java "
+ OptionMaskCallbackReader.class.getName() + " [option-name-to-mask]");
}

public static void main(String[] args) throws IOException {
Terminal terminal = TerminalBuilder.terminal();
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal).build();

String optionToMask = args[0];

MaskingCallback maskingCallback = new OptionValueMask(optionToMask);

String line;
do {
line = reader.readLine("prompt> ", null, maskingCallback, null);
System.out.println("Got line: " + line);
}
while (line != null && line.length() > 0);
}

private static class OptionValueMask implements MaskingCallback {

private final Pattern pattern;
private static final Character mask = '*';

public OptionValueMask(String option) {
String patternString= ".*?" + Pattern.quote(option) + " ??([^ ]+)";
this.pattern = Pattern.compile(patternString);
}

@Override
public String display(String line) {
return filter(line);
}

@Override
public String history(String line)
{
final String filter = filter(line);
System.out.println();
System.out.print("Adding to history: " + filter );
return filter;
}

public String filter(String line) {
Matcher m = pattern.matcher(line);

if( m.find() ) {
StringBuilder maskedLine = new StringBuilder(line);
for(int i = m.start(1); i < m.end(1); i++){
maskedLine.replace(i,i+1, String.valueOf(mask));
}
return maskedLine.toString();
} else {
return line;
}
}
}

}
20 changes: 20 additions & 0 deletions reader/src/main/java/org/jline/reader/LineReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,26 @@ enum RegionType {
*/
String readLine(String prompt, String rightPrompt, Character mask, String buffer) throws UserInterruptException, EndOfFileException;

/**
* Read a line from the <i>in</i> {@link InputStream}, and return the line
* (without any trailing newlines).
*
* @param prompt The prompt to issue to the terminal, may be null.
* This is a template, with optional {@code '%'} escapes, as
* described in the class header.
* @param rightPrompt The right prompt
* This is a template, with optional {@code '%'} escapes, as
* described in the class header.
* @param maskingCallback The {@link MaskingCallback} to use when displaying lines and adding them to the line {@link History}
* @param buffer The default value presented to the user to edit, may be null.
* @return A line that is read from the terminal, can never be null.
*
* @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example)
* @throws EndOfFileException if an EOF has been found (using Ctrl-D for example)
* @throws java.io.IOError in case of other i/o errors
*/
String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) throws UserInterruptException, EndOfFileException;

void callWidget(String name);

Map<String, Object> getVariables();
Expand Down
27 changes: 27 additions & 0 deletions reader/src/main/java/org/jline/reader/MaskingCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2002-2017, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.reader;

/**
* Callback used to mask parts of the line
*/
public interface MaskingCallback {

/**
* Transforms the line before it is displayed so that
* some parts can be hidden.
*/
String display(String line);

/**
* Transforms the line before storing in the history.
*/
String history(String line);

}
68 changes: 28 additions & 40 deletions reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ protected enum BellType {
protected AttributedString prompt;
protected AttributedString rightPrompt;

protected Character mask;
protected MaskingCallback maskingCallback;

protected Map<Integer, String> modifiedHistory = new HashMap<>();
protected Buffer historyBuffer = null;
Expand Down Expand Up @@ -375,7 +375,7 @@ public void setExpander(Expander expander) {
* Read the next line and return the contents of the buffer.
*/
public String readLine() throws UserInterruptException, EndOfFileException {
return readLine(null, null, null, null);
return readLine(null, null, (MaskingCallback) null, null);
}

/**
Expand All @@ -387,7 +387,7 @@ public String readLine(Character mask) throws UserInterruptException, EndOfFileE
}

public String readLine(String prompt) throws UserInterruptException, EndOfFileException {
return readLine(prompt, null, null, null);
return readLine(prompt, null, (MaskingCallback) null, null);
}

/**
Expand Down Expand Up @@ -423,10 +423,14 @@ public String readLine(String prompt, Character mask, String buffer) throws User
* was pressed).
*/
public String readLine(String prompt, String rightPrompt, Character mask, String buffer) throws UserInterruptException, EndOfFileException {
return readLine(prompt, rightPrompt, mask != null ? new SimpleMaskingCallback(mask) : null, buffer);
}

public String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) throws UserInterruptException, EndOfFileException {
// prompt may be null
// mask may be null
// maskingCallback may be null
// buffer may be null

Thread readLineThread = Thread.currentThread();
SignalHandler previousIntrHandler = null;
SignalHandler previousWinchHandler = null;
Expand All @@ -439,7 +443,7 @@ public String readLine(String prompt, String rightPrompt, Character mask, String
}
reading = true;

this.mask = mask;
this.maskingCallback = maskingCallback;

/*
* This is the accumulator for VI-mode repeat count. That is, while in
Expand Down Expand Up @@ -857,10 +861,12 @@ protected String finishBuffer() {
str = sb.toString();
}

if (maskingCallback != null) {
historyLine = maskingCallback.history(historyLine);
}

// we only add it to the history if the buffer is not empty
// and if mask is null, since having a mask typically means
// the string was a password. We clear the mask after this call
if (str.length() > 0 && mask == null) {
if (historyLine != null && historyLine.length() > 0 ) {
history.add(Instant.now(), historyLine);
}
return str;
Expand Down Expand Up @@ -3322,7 +3328,12 @@ protected void redisplay(boolean flush) {

sb.setLength(0);
sb.append(prompt);
concat(getMaskedBuffer(buf.upToCursor()).columnSplitLength(Integer.MAX_VALUE), sb);
String line = buf.upToCursor();
if(maskingCallback != null) {
line = maskingCallback.display(line);
}

concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb);
AttributedString toCursor = sb.toAttributedString();

int w = WCWidth.wcwidth('…');
Expand Down Expand Up @@ -3383,16 +3394,8 @@ protected void redisplay(boolean flush) {
AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
sb.append(prompt);
String buffer = buf.upToCursor();
if (mask != null) {
if (mask == NULL_MASK) {
buffer = "";
} else {
StringBuilder nsb = new StringBuilder();
for (int i = buffer.length(); i-- > 0; ) {
nsb.append((char) mask);
}
buffer = nsb.toString();
}
if (maskingCallback != null) {
buffer = maskingCallback.display(buffer);
}
sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false));
List<AttributedString> promptLines = sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap());
Expand Down Expand Up @@ -3431,29 +3434,14 @@ private AttributedString getDisplayedBufferWithPrompts(List<AttributedString> se
return full.toAttributedString();
}

private AttributedString getMaskedBuffer(String buffer) {
if (mask != null) {
if (mask == NULL_MASK) {
buffer = "";
} else {
StringBuilder sb = new StringBuilder();
for (int i = buffer.length(); i-- > 0;) {
sb.append((char) mask);
}
buffer = sb.toString();
}
}
return new AttributedString(buffer);
}

private AttributedString getHighlightedBuffer(String buffer) {
if (mask != null) {
return getMaskedBuffer(buffer);
} else if (highlighter != null && !isSet(Option.DISABLE_HIGHLIGHTER)) {
if (maskingCallback != null) {
buffer = maskingCallback.display(buffer);
}
if (highlighter != null && !isSet(Option.DISABLE_HIGHLIGHTER)) {
return highlighter.highlight(this, buffer);
} else {
return new AttributedString(buffer);
}
return new AttributedString(buffer);
}

private AttributedString expandPromptPattern(String pattern, int padToWidth,
Expand Down
Loading

0 comments on commit 3793dcd

Please sign in to comment.