Skip to content

Commit

Permalink
GROOVY-7567: groovysh: Fix file completion with blanks, use escaping …
Browse files Browse the repository at this point in the history
…instead of hyphens (closes apache#92)

Trying to close hyphens leads to complex/buggy code. As an example, completing:
```:load "foo|```
with candidate file 'foo bar' leads to
```:load "'foo bar'```
because jline does not pass the initial hyphen to the FileNameCompleter.
  • Loading branch information
tkruse authored and PascalSchumacher committed Sep 1, 2015
1 parent 01ea7ac commit e5acbaa
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 56 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ The following class within this product:
org.codehaus.groovy.tools.shell.completion.FileNameCompleter

was derived from JLine 2.12, and the following patch:
https://github.com/jline/jline2/issues/90
https://github.com/jline/jline2/pull/204
JLine2 is made available under a BSD License.
For details, see licenses/jline2-license.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class LoadCommand

@Override
protected List<Completer> createCompleters() {
return [ new FileNameCompleter(true, true) ]
return [ new FileNameCompleter(true) ]
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ import static jline.internal.Preconditions.checkNotNull
/**
* PATCHED copy from jline 2.12, with
* https://github.com/jline/jline2/issues/90 (no trailing blank)
* https://github.com/jline/jline2/pull/204
*
* NOTE: we hope to work with the jline project to have this functionality
* absorbed into a future jline release and then remove this file, so keep
* that in mind if you are thinking of changing this file.
*
*/

/**
* A file name completer takes the buffer and issues a list of
* potential completions.
* <p/>
Expand All @@ -62,14 +66,10 @@ import static jline.internal.Preconditions.checkNotNull
public class FileNameCompleter
implements Completer
{
// TODO: Handle files with spaces in them

private static final boolean OS_IS_WINDOWS;

private boolean printSpaceAfterFullCompletion = true;

private boolean handleLeadingHyphen = false;

public boolean getPrintSpaceAfterFullCompletion() {
return printSpaceAfterFullCompletion;
}
Expand All @@ -91,15 +91,9 @@ implements Completer
}


public FileNameCompleter(boolean blankSuffix, boolean handleLeadingHyphen) {
this(blankSuffix)
this.handleLeadingHyphen = handleLeadingHyphen
}

public int complete(String buffer, final int cursor, final List<CharSequence> candidates) {
// buffer can be null
checkNotNull(candidates);
String hyphenChar = null;

if (buffer == null) {
buffer = "";
Expand All @@ -110,10 +104,6 @@ implements Completer
}

String translated = buffer;
if (handleLeadingHyphen && (translated.startsWith('\'') || translated.startsWith('"'))) {
hyphenChar = translated[0];
translated = translated.substring(1);
}

// Special character: ~ maps to the user's home directory in most OSs
if (!OS_IS_WINDOWS && translated.startsWith("~")) {
Expand Down Expand Up @@ -142,7 +132,7 @@ implements Completer

File[] entries = (dir == null) ? new File[0] : dir.listFiles();

return matchFiles(buffer, translated, entries, candidates, hyphenChar);
return matchFiles(buffer, translated, entries, candidates);
}

protected static String separator() {
Expand All @@ -153,36 +143,31 @@ implements Completer
return Configuration.getUserHome();
}

protected static File getUserDir() {
/*
* non static for testing
*/
protected File getUserDir() {
return new File(".");
}

protected int matchFiles(final String buffer, final String translated, final File[] files, final List<CharSequence> candidates, final String hyphenChar) {
protected int matchFiles(final String buffer, final String translated, 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++;
}
}
for (File file : files) {
if (file.getAbsolutePath().startsWith(translated)) {
CharSequence name = file.getName()
if (matches == 1) {
if (file.isDirectory()) {
name += separator();
} else {
if (printSpaceAfterFullCompletion && !hyphenChar) {
name += ' ';
}
CharSequence name = file.getName();
String renderedName = render(name).toString();
if (file.isDirectory()) {
renderedName += separator();
} else {
if (printSpaceAfterFullCompletion) {
renderedName += ' '
}
}
candidates.add(render(name, hyphenChar).toString());

candidates.add(renderedName);
}
}

Expand All @@ -191,18 +176,21 @@ implements Completer
return index + separator().length();
}

protected CharSequence render(final CharSequence name, final String hyphenChar) {
if (hyphenChar != null) {
return escapedNameInHyphens(name, hyphenChar);
}
if (name.contains(' ')) {
return escapedNameInHyphens(name, '\'');
}
return name;
/**
* @param name
* @param hyphenChar force hyphenation with this if not null
* @return name in hyphens if it contains a blank
*/
protected static CharSequence render(final CharSequence name) {
return escapedName(name);
}

private String escapedNameInHyphens(final CharSequence name, String hyphen) {
// need to escape every instance of chartoEscape, and every instance of the escape char backslash
return hyphen + name.toString().replace('\\', '\\\\').replace(hyphen, '\\' + hyphen) + hyphen
/**
*
* @return name in hyphens Strings with hyphens and backslashes escaped
*/
private static String escapedName(final CharSequence name) {
// Escape blanks, hyphens and escape characters
return name.toString().replace('\\', '\\\\').replace('"', '\\"').replace('\'', '\\\'').replace(' ', '\\ ')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,48 @@
*/
package org.codehaus.groovy.tools.shell.completion

import org.junit.rules.TemporaryFolder

class FileNameCompleterTest extends GroovyTestCase {

void testRender() {
FileNameCompleter completer = new FileNameCompleter()
assert completer.render('foo', null) == 'foo'
assert completer.render('foo bar', null) == '\'foo bar\''
assert completer.render('foo \'bar', null) == '\'foo \\\'bar\''
assert completer.render('foo \'bar', '\'') == '\'foo \\\'bar\''
assert completer.render('foo " \'bar', '"') == '"foo \\" \'bar"'
assert FileNameCompleter.render('foo') == 'foo'
assert FileNameCompleter.render('foo bar') == 'foo\\ bar'
// intentionally adding empty String, to get better power assert output
assert FileNameCompleter.render('foo \'\"bar') == 'foo\\ \\\'\\\"bar' + ''
}

void testCompletionNoFiles() {
// abusing junit testrule
TemporaryFolder testFolder = null;
try {
testFolder = new TemporaryFolder();
testFolder.create()

FileNameCompleter completor = new FileNameCompleter() {
@Override
protected File getUserDir() {
return testFolder.getRoot()
}
}
def candidates = []
String buffer = ''
assert 0 == completor.complete(buffer, 0, candidates)
assert [] == candidates
} finally {
if (testFolder != null) {
testFolder.delete()
}
}
}

void testMatchFiles_Unix() {
if(! System.getProperty('os.name').startsWith('Windows')) {
FileNameCompleter completer = new FileNameCompleter()
List<String> candidates = []
int resultIndex = completer.matchFiles('foo/bar', '/foo/bar', [new File('/foo/baroo'), new File('/foo/barbee')] as File[], candidates, null)
int resultIndex = completer.matchFiles('foo/bar', '/foo/bar', [new File('/foo/baroo'), new File('/foo/barbee')] as File[], candidates)
assert resultIndex == 'foo/'.length()
assert candidates == ['baroo', 'barbee']
assert candidates == ['baroo ', 'barbee ']
}
}
}

0 comments on commit e5acbaa

Please sign in to comment.