Repository: ipython/ipython

Issue:
Filename completion with quotes in filename does not escape the quotes
When completing filenames containing quotes those are completed verbatim, closing the string.
The user has to go back in the string to escape the quotes manually.

```
$ touch /tmp/foo\'bar
$ ipython
In [1]: open('/tmp/foo<TAB>
# actual
In [1]: open('/tmp/foo'bar
# Expected:
In [1]: open('/tmp/foo\'bar
```

```python
{'commit_hash': '3a3686a',
 'commit_source': 'repository',
 'default_encoding': 'UTF-8',
 'ipython_path': '/home/t-8ch/Projekte/ipython/IPython',
 'ipython_version': '6.0.0.dev',
 'os_name': 'posix',
 'platform': 'Linux-4.8.10-1-ARCH-x86_64-with-arch',
 'sys_executable': '/home/t-8ch/.local/share/virtualenvs/ipython/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.5.2 (default, Nov  7 2016, 11:31:36) \n[GCC 6.2.1 20160830]'}
```


Discussion:
Windows escapes a string by wrapping it with a double quote. In particular, Windows is only concerned with spaces in a string.

So what does that mean for the sample string? Are the surrounding single quotes replaced by double quotes? I'm not sure how to interpret this.

@Carreau says [we should only protect on non-Windows](https://github.com/ipython/ipython/pull/10533#issuecomment-300855578).
> @Carreau says we should only protect on non-Windows.

not sure for "should", but if we are unsure we can always improve the experience on unix-like without changing the experience on windows, and decide about the windows codepath later :-) 

Resolving patch:
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -226,13 +226,13 @@ def has_open_quotes(s):
         return False
 
 
-def protect_filename(s):
+def protect_filename(s, protectables=PROTECTABLES):
     """Escape a string to protect certain characters."""
-    if set(s) & set(PROTECTABLES):
+    if set(s) & set(protectables):
         if sys.platform == "win32":
             return '"' + s + '"'
         else:
-            return "".join(("\\" + c if c in PROTECTABLES else c) for c in s)
+            return "".join(("\\" + c if c in protectables else c) for c in s)
     else:
         return s
 
@@ -1133,9 +1133,10 @@ def file_matches(self, text):
         else:
             if open_quotes:
                 # if we have a string with an open quote, we don't need to
-                # protect the names at all (and we _shouldn't_, as it
-                # would cause bugs when the filesystem call is made).
-                matches = m0
+                # protect the names beyond the quote (and we _shouldn't_, as
+                # it would cause bugs when the filesystem call is made).
+                matches = m0 if sys.platform == "win32" else\
+                    [protect_filename(f, open_quotes) for f in m0]
             else:
                 matches = [text_prefix +
                            protect_filename(f) for f in m0]


Test patch:
diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py
--- a/IPython/core/tests/test_completer.py
+++ b/IPython/core/tests/test_completer.py
@@ -274,6 +274,37 @@ def test_local_file_completions():
         nt.assert_true(comp.issubset(set(c)))
 
 
+def test_quoted_file_completions():
+    ip = get_ipython()
+    with TemporaryWorkingDirectory():
+        name = "foo'bar"
+        open(name, 'w').close()
+
+        # Don't escape Windows
+        escaped = name if sys.platform == "win32" else "foo\\'bar"
+
+        # Single quote matches embedded single quote
+        text = "open('foo"
+        c = ip.Completer._complete(cursor_line=0,
+                                   cursor_pos=len(text),
+                                   full_text=text)[1]
+        nt.assert_equal(c, [escaped])
+
+        # Double quote requires no escape
+        text = 'open("foo'
+        c = ip.Completer._complete(cursor_line=0,
+                                   cursor_pos=len(text),
+                                   full_text=text)[1]
+        nt.assert_equal(c, [name])
+
+        # No quote requires an escape
+        text = '%ls foo'
+        c = ip.Completer._complete(cursor_line=0,
+                                   cursor_pos=len(text),
+                                   full_text=text)[1]
+        nt.assert_equal(c, [escaped])
+
+
 def test_jedi():
     """
     A couple of issue we had with Jedi