Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 82 additions & 21 deletions src/main/java/jline/console/completer/ArgumentCompleter.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,16 @@ public int complete(final String buffer, final int cursor, final List<CharSequen
else {
completer = completers.get(argIndex);
}
if (completer == null) {
return -1;
}

// ensure that all the previous completers are successful before allowing this completer to pass (only if strict).
for (int i = 0; isStrict() && (i < argIndex); i++) {
Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i);
if (sub == null) {
continue;
}
String[] args = list.getArguments();
String arg = (args == null || i >= args.length) ? "" : args[i];

Expand All @@ -149,11 +155,30 @@ public int complete(final String buffer, final int cursor, final List<CharSequen
}
}

int ret = completer.complete(list.getCursorArgument(), argpos, candidates);
List<CharSequence> subCandidates = new LinkedList<CharSequence>();
int ret = completer.complete(list.getCursorArgument(), argpos, subCandidates);

if (ret == -1) {
return -1;
}
if (list.argHyphen == null) {
// Since the completer does not know whether it has to escape or not, assume it has not escaped.
// what needs escaping is every char that would cause the completion create more than one argument.
for (CharSequence subCandidate : subCandidates) {
candidates.add(getDelimiter().escapeArgument(subCandidate));
}
} else {
for (CharSequence subCandidate : subCandidates) {
if (getDelimiter().isDelimiter(subCandidate, subCandidate.length() - 1)) {
candidates.add(subCandidate.subSequence(0, subCandidate.length() - 1).toString()
+ list.argHyphen + subCandidate.charAt(subCandidate.length() - 1));
} else {
// must not add hyphen as completion might not be finished,
// e.g. filepath completion can go into next subfolder
candidates.add(subCandidate.toString());
}
}
}

int pos = ret + list.getBufferPosition() - argpos;

Expand Down Expand Up @@ -205,6 +230,16 @@ public static interface ArgumentDelimiter
* @return True if the character should be a delimiter
*/
boolean isDelimiter(CharSequence buffer, int pos);

/**
* Returns a modification of argument where escaping characters have been added as necessary
* such that isDelimiter() is false for all characters but the last, which may be true or false
* (delimiting towards the following argument).
*
* @param argument A string that represents an unescaped argument without trailing delimiters.
* @return The argument escaped
*/
CharSequence escapeArgument(CharSequence argument);
}

/**
Expand Down Expand Up @@ -286,54 +321,53 @@ public ArgumentList delimit(final CharSequence buffer, final int cursor) {
// length of the current argument
argpos = arg.length();
}
CharSequence argHyphen = null;
if (arg.length() > 0) {
// still in open quote block
if (quoteStart >= 0) {
argHyphen = buffer.subSequence(quoteStart, quoteStart + 1);
}
args.add(arg.toString());
}

return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor);
return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor, argHyphen);
}

/**
* Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not
* escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and
* returns true from {@link #isDelimiterChar}.
* escaped by any {@link #getEscapeChars}, and returns true from {@link #isDelimiterChar}.
* Whether it delimits arguments or is within a quote context is decided elsewhere.
*
* @param buffer The complete command buffer
* @param pos The index of the character in the buffer
* @return True if the character should be a delimiter
*/
public boolean isDelimiter(final CharSequence buffer, final int pos) {
return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
}

public boolean isQuoted(final CharSequence buffer, final int pos) {
return false;
}

public boolean isQuoteChar(final CharSequence buffer, final int pos) {
if (pos < 0) {
return false;
}

for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) {
if (buffer.charAt(pos) == quoteChars[i]) {
return !isEscaped(buffer, pos);
}
}
return isDelimiterChar(buffer, pos) && !isEscaped(buffer, pos);
}

return false;
public boolean isQuoteChar(final CharSequence buffer, final int pos) {
return isUnescapedCharInArray(buffer, pos, quoteChars);
}

/**
* Check if this character is a valid escape char (i.e. one that has not been escaped)
*/
public boolean isEscapeChar(final CharSequence buffer, final int pos) {
return isUnescapedCharInArray(buffer, pos, escapeChars);
}

protected boolean isUnescapedCharInArray(final CharSequence buffer, final int pos, char[] array) {
if (pos < 0) {
return false;
}

for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) {
if (buffer.charAt(pos) == escapeChars[i]) {
for (int i = 0; (array != null) && (i < array.length); i++) {
if (buffer.charAt(pos) == array[i]) {
return !isEscaped(buffer, pos); // escape escape
}
}
Expand All @@ -359,6 +393,21 @@ public boolean isEscaped(final CharSequence buffer, final int pos) {
return isEscapeChar(buffer, pos - 1);
}

public CharSequence escapeArgument(CharSequence argument) {
if (escapeChars == null || escapeChars.length == 0) {
return argument;
}
StringBuilder builder = new StringBuilder(argument.length());
for (int i = 0; (argument != null) && (i < argument.length() - 1); i++) {
if ((isDelimiterChar(argument, i)) || isEscapeChar(argument, i) || isQuoteChar(argument, i)) {
builder.append(escapeChars[0]);
}
builder.append(argument.charAt(i));
}
builder.append(argument.charAt(argument.length() - 1));
return builder.toString();
}

/**
* Returns true if the character at the specified position if a delimiter. This method will only be called if
* the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
Expand All @@ -382,6 +431,9 @@ public static class WhitespaceArgumentDelimiter
*/
@Override
public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
if (pos < 0) {
return false;
}
return Character.isWhitespace(buffer.charAt(pos));
}
}
Expand All @@ -393,6 +445,7 @@ public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
*/
public static class ArgumentList
{

private String[] arguments;

private int cursorArgumentIndex;
Expand All @@ -401,17 +454,25 @@ public static class ArgumentList

private int bufferPosition;

private final CharSequence argHyphen;

/**
* @param arguments The array of tokens
* @param cursorArgumentIndex The token index of the cursor
* @param argumentPosition The position of the cursor in the current token
* @param bufferPosition The position of the cursor in the whole buffer
* @param argHyphen The opening hyphen of last argument if not closed, else null
*/
public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition, final CharSequence argHyphen) {
this.arguments = checkNotNull(arguments);
this.cursorArgumentIndex = cursorArgumentIndex;
this.argumentPosition = argumentPosition;
this.bufferPosition = bufferPosition;
this.argHyphen = argHyphen;
}

public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
this(arguments, cursorArgumentIndex, argumentPosition, bufferPosition, null);
}

public void setCursorArgumentIndex(final int i) {
Expand Down
108 changes: 89 additions & 19 deletions src/main/java/jline/console/completer/FileNameCompleter.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,48 @@
public class FileNameCompleter
implements Completer
{
// TODO: Handle files with spaces in them

private static final boolean OS_IS_WINDOWS;

/**
* If true, will folders as final completion result
*/
private boolean completeFolders = false;

/**
* If false, will not offer files
*/
private boolean completeFiles = true;

/**
* whether to append a blank after full completion (depending on whether more arguments will follow this one)
*/
private boolean printSpaceAfterFullCompletion = true;

public boolean getCompleteFolders() {
return completeFolders;
}

public void setCompleteFolders(boolean completeFolders) {
this.completeFolders = completeFolders;
}

public boolean getCompleteFiles() {
return completeFiles;
}

public void setCompleteFiles(boolean completeFiles) {
this.completeFiles = completeFiles;
}

public boolean getPrintSpaceAfterFullCompletion() {
return printSpaceAfterFullCompletion;
}

public void setPrintSpaceAfterFullCompletion(boolean printSpaceAfterFullCompletion) {
this.printSpaceAfterFullCompletion = printSpaceAfterFullCompletion;
}

static {
String os = Configuration.getOsName();
OS_IS_WINDOWS = os.contains("windows");
Expand All @@ -61,14 +99,15 @@ public int complete(String buffer, final int cursor, final List<CharSequence> ca

String translated = buffer;

File homeDir = getUserHome();

// Special character: ~ maps to the user's home directory
if (translated.startsWith("~" + separator())) {
translated = homeDir.getPath() + translated.substring(1);
}
else if (translated.startsWith("~")) {
translated = homeDir.getParentFile().getAbsolutePath();
// Special character: ~ maps to the user's home directory in most OSs
if (!OS_IS_WINDOWS && translated.startsWith("~")) {
File homeDir = getUserHome();
if (translated.startsWith("~" + separator())) {
translated = homeDir.getPath() + translated.substring(1);
}
else {
translated = homeDir.getParentFile().getAbsolutePath();
}
}
else if (!(new File(translated).isAbsolute())) {
String cwd = getUserDir().getAbsolutePath();
Expand Down Expand Up @@ -102,23 +141,40 @@ protected File getUserDir() {
return new File(".");
}

protected int matchFiles(final String buffer, final String translated, final File[] files, final List<CharSequence> candidates) {
protected int matchFiles(final String buffer, final String prefix, final File[] files, final List<CharSequence> candidates) {
if (files == null) {
return -1;
}

int matches = 0;

// first pass: just count the matches
for (File file : files) {
if (file.getAbsolutePath().startsWith(translated)) {
matches++;
if (!completeFiles && !file.isDirectory()) {
continue;
}
}
for (File file : files) {
if (file.getAbsolutePath().startsWith(translated)) {
CharSequence name = file.getName() + (matches == 1 && file.isDirectory() ? separator() : " ");
candidates.add(render(file, name).toString());
if (ignoreFile(file)) {
continue;
}
if (file.getAbsolutePath().startsWith(prefix)) {
String renderedName = render(file, file.getName()).toString();
if (file.isDirectory()) {
// add first candidate folder with separator for file/subfolder
if (completeFiles || hasSubfolders(file)) {
// render separator only if has subfolders
candidates.add(renderedName + separator());
}
// add second candidate (folder itself)
if (completeFolders) {
if (printSpaceAfterFullCompletion) {
renderedName += ' ';
}
candidates.add(renderedName);
}
} else {
if (printSpaceAfterFullCompletion) {
renderedName += ' ';
}
candidates.add(renderedName);
}
}
}

Expand All @@ -127,6 +183,20 @@ protected int matchFiles(final String buffer, final String translated, final Fil
return index + separator().length();
}

// hook to extend Filename COmpleter to exclude certain files / folders
protected boolean ignoreFile(File file) {
return false;
}

protected boolean hasSubfolders(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
return true;
}
}
return false;
}

protected CharSequence render(final File file, final CharSequence name) {
return name;
}
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/jline/internal/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ private static Properties initProperties() {

private static void loadProperties(final URL url, final Properties props) throws IOException {
Log.debug("Loading properties from: ", url);
InputStream input = url.openStream();
InputStream input;
try {
input = url.openStream();
} catch (IOException e) {
Log.debug("Could not load properties from " + url + " : " + e.getMessage());
return;
}
try {
props.load(new BufferedInputStream(input));
}
Expand Down
8 changes: 7 additions & 1 deletion src/test/java/jline/console/ConsoleReaderTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

import jline.TerminalSupport;
import org.junit.Before;
Expand Down Expand Up @@ -83,7 +84,7 @@ protected void assertPosition(int pos, final Buffer buffer, final boolean clear)
// noop
}

assertEquals(pos, console.getCursorPosition ());
assertEquals(pos, console.getCursorPosition());
}

/**
Expand Down Expand Up @@ -115,6 +116,11 @@ protected void assertLine(final String expected, final Buffer buffer,
assertEquals(expected, prevLine);
}

protected void assertEqualSet(List<?> l1, List<?> l2) {
assertTrue(l1.containsAll(l2) && l2.containsAll(l1));
}


private String getKeyForAction(final Operation key) {
switch (key) {
case BACKWARD_WORD: return "\u001Bb";
Expand Down
Loading