Skip to content

Commit

Permalink
Fix bug in tab completion of filenames when quotes are present.
Browse files Browse the repository at this point in the history
Unit tests added to the completion code as well, to validate quote
handling more stringently.
  • Loading branch information
fperez committed Oct 26, 2010
1 parent ae65e73 commit 02eecaf
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 24 deletions.
79 changes: 55 additions & 24 deletions IPython/core/completer.py
Expand Up @@ -101,6 +101,27 @@
# Main functions and classes
#-----------------------------------------------------------------------------

def has_open_quotes(s):
"""Return whether a string has open quotes.
This simply counts whether the number of quote characters of either type in
the string is odd.
Returns
-------
If there is an open quote, the quote character is returned. Else, return
False.
"""
# We check " first, then ', so complex cases with nested quotes will get
# the " to take precedence.
if s.count('"') % 2:
return '"'
elif s.count("'") % 2:
return "'"
else:
return False


def protect_filename(s):
"""Escape a string to protect certain characters."""

Expand Down Expand Up @@ -485,39 +506,41 @@ def file_matches(self, text):
text_prefix = '!'
else:
text_prefix = ''

text_until_cursor = self.text_until_cursor
open_quotes = 0 # track strings with open quotes
try:
# arg_split ~ shlex.split, but with unicode bugs fixed by us
lsplit = arg_split(text_until_cursor)[-1]
except ValueError:
# typically an unmatched ", or backslash without escaped char.
if text_until_cursor.count('"')==1:
open_quotes = 1
lsplit = text_until_cursor.split('"')[-1]
elif text_until_cursor.count("'")==1:
open_quotes = 1
lsplit = text_until_cursor.split("'")[-1]
else:
return []
except IndexError:
# tab pressed on empty line
lsplit = ""
# track strings with open quotes
open_quotes = has_open_quotes(text_until_cursor)

if '(' in text_until_cursor or '[' in text_until_cursor:
lsplit = text
else:
try:
# arg_split ~ shlex.split, but with unicode bugs fixed by us
lsplit = arg_split(text_until_cursor)[-1]
except ValueError:
# typically an unmatched ", or backslash without escaped char.
if open_quotes:
lsplit = text_until_cursor.split(open_quotes)[-1]
else:
return []
except IndexError:
# tab pressed on empty line
lsplit = ""

if not open_quotes and lsplit != protect_filename(lsplit):
# if protectables are found, do matching on the whole escaped
# name
has_protectables = 1
# if protectables are found, do matching on the whole escaped name
has_protectables = True
text0,text = text,lsplit
else:
has_protectables = 0
has_protectables = False
text = os.path.expanduser(text)

if text == "":
return [text_prefix + protect_filename(f) for f in self.glob("*")]

# Compute the matches from the filesystem
m0 = self.clean_glob(text.replace('\\',''))

if has_protectables:
# If we had protectables, we need to revert our changes to the
# beginning of filename so that we don't double-write the part
Expand Down Expand Up @@ -711,7 +734,7 @@ def dispatch_custom_completer(self, text):
return None

def complete(self, text=None, line_buffer=None, cursor_pos=None):
"""Return the state-th possible completion for 'text'.
"""Find completions for the given text and line context.
This is called successively with state == 0, 1, 2, ... until it
returns None. The completion should begin with 'text'.
Expand All @@ -734,6 +757,14 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None):
cursor_pos : int, optional
Index of the cursor in the full line buffer. Should be provided by
remote frontends where kernel has no access to frontend state.
Returns
-------
text : str
Text that was actually used in the completion.
matches : list
A list of completion matches.
"""
#io.rprint('\nCOMP1 %r %r %r' % (text, line_buffer, cursor_pos)) # dbg

Expand Down Expand Up @@ -772,7 +803,7 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None):
except:
# Show the ugly traceback if the matcher causes an
# exception, but do NOT crash the kernel!
sys.excepthook()
sys.excepthook(*sys.exc_info())
else:
for matcher in self.matchers:
self.matches = matcher(text)
Expand Down
43 changes: 43 additions & 0 deletions IPython/core/tests/test_completer.py
Expand Up @@ -5,6 +5,7 @@
#-----------------------------------------------------------------------------

# stdlib
import os
import sys
import unittest

Expand All @@ -13,6 +14,7 @@

# our own packages
from IPython.core import completer
from IPython.utils.tempdir import TemporaryDirectory

#-----------------------------------------------------------------------------
# Test functions
Expand Down Expand Up @@ -113,3 +115,44 @@ def test_spaces(self):
('run foo', 'bar', 'foo'),
]
check_line_split(self.sp, t)


def test_has_open_quotes1():
for s in ["'", "'''", "'hi' '"]:
nt.assert_equal(completer.has_open_quotes(s), "'")


def test_has_open_quotes2():
for s in ['"', '"""', '"hi" "']:
nt.assert_equal(completer.has_open_quotes(s), '"')


def test_has_open_quotes3():
for s in ["''", "''' '''", "'hi' 'ipython'"]:
nt.assert_false(completer.has_open_quotes(s))


def test_has_open_quotes4():
for s in ['""', '""" """', '"hi" "ipython"']:
nt.assert_false(completer.has_open_quotes(s))


def test_file_completions():

ip = get_ipython()
with TemporaryDirectory() as tmpdir:
prefix = os.path.join(tmpdir, 'foo')
suffixes = map(str, [1,2])
names = [prefix+s for s in suffixes]
for n in names:
open(n, 'w').close()

# Check simple completion
c = ip.complete(prefix)[1]
nt.assert_equal(c, names)

# Now check with a function call
cmd = 'a = f("%s' % prefix
c = ip.complete(prefix, cmd)[1]
comp = [prefix+s for s in suffixes]
nt.assert_equal(c, comp)

0 comments on commit 02eecaf

Please sign in to comment.