From e25c99a4545ed475e69d5da8bddb1184f3f0462a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaj=C4=85c?= Date: Tue, 19 May 2015 18:07:04 +0200 Subject: [PATCH 1/7] stub of completion from manpages --- xonsh/completer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/xonsh/completer.py b/xonsh/completer.py index fac917d9e..36f90528e 100644 --- a/xonsh/completer.py +++ b/xonsh/completer.py @@ -94,6 +94,8 @@ class Completer(object): elif prefix.startswith('${') or prefix.startswith('@('): # python mode explicitly rtn = set() + elif prefix.startswith('-'): + return sorted(self.option_complete(prefix, cmd)) elif cmd not in ctx: if cmd == 'import' and begidx == len('import '): # completing module to import @@ -136,10 +138,19 @@ class Completer(object): return {s + space for s in self._all_commands() if s.startswith(cmd)} def module_complete(self, prefix): - """Completes a name of a module to import""" + """Completes a name of a module to import.""" modules = set(sys.modules.keys()) return {s for s in modules if s.startswith(prefix)} + def option_complete(self, prefix, cmd): + """Completes an option name, basing on content of man page.""" + manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE) + # This is a trick to get rid of reverse line feeds + text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') + pattern = r'^\s*(-\w|--[a-z-]+)[\s,]' + matches = re.findall(pattern, text, re.M) + return {s for s in matches if s.startswith(prefix)} + def path_complete(self, prefix): """Completes based on a path name.""" space = ' ' # intern some strings for faster appending From 9ae796e4770ed86ac62cb8e9793335d7cd4f9b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaj=C4=85c?= Date: Tue, 19 May 2015 18:07:30 +0200 Subject: [PATCH 2/7] updated gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f63f7c23e..bebf3160a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ pip-selfcheck.json bin/ lib/ include/ + +# Mac +.DS_Store \ No newline at end of file From 34502396727393942721f881b39e2f08a775a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaj=C4=85c?= Date: Mon, 25 May 2015 12:51:07 +0200 Subject: [PATCH 3/7] caching to disk and enhanced RE --- xonsh/completer.py | 52 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/xonsh/completer.py b/xonsh/completer.py index 36f90528e..b8a422045 100644 --- a/xonsh/completer.py +++ b/xonsh/completer.py @@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals import os import re import builtins +import pickle import subprocess import sys @@ -43,6 +44,7 @@ class Completer(object): self._alias_checksum = None self._path_mtime = -1 self._cmds_cache = frozenset() + self._man_completer = ManCompleter() try: # FIXME this could be threaded for faster startup times self._load_bash_complete_funcs() @@ -95,7 +97,7 @@ class Completer(object): # python mode explicitly rtn = set() elif prefix.startswith('-'): - return sorted(self.option_complete(prefix, cmd)) + return sorted(self._man_completer.option_complete(prefix, cmd)) elif cmd not in ctx: if cmd == 'import' and begidx == len('import '): # completing module to import @@ -142,15 +144,6 @@ class Completer(object): modules = set(sys.modules.keys()) return {s for s in modules if s.startswith(prefix)} - def option_complete(self, prefix, cmd): - """Completes an option name, basing on content of man page.""" - manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE) - # This is a trick to get rid of reverse line feeds - text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') - pattern = r'^\s*(-\w|--[a-z-]+)[\s,]' - matches = re.findall(pattern, text, re.M) - return {s for s in matches if s.startswith(prefix)} - def path_complete(self, prefix): """Completes based on a path name.""" space = ' ' # intern some strings for faster appending @@ -309,3 +302,42 @@ class Completer(object): allcmds |= set(builtins.aliases.keys()) self._cmds_cache = frozenset(allcmds) return self._cmds_cache + + +OPTIONS_PATH = os.path.expanduser('~') + "/.xonsh_man_completions" +SCRAPE_RE = re.compile(r'^(?:\s*(?:-\w|--[a-z0-9-]+)[\s,])+', re.M) +INNER_OPTIONS_RE = re.compile(r'-\w|--[a-z0-9-]+') + + +class ManCompleter(object): + """Helper class that loads completions derived from man pages.""" + + def __init__(self): + self._load_cached_options() + + def __del__(self): + self._save_cached_options() + + def option_complete(self, prefix, cmd): + """Completes an option name, basing on content of man page.""" + if cmd not in self._options.keys(): + manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE) + # This is a trick to get rid of reverse line feeds + text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') + scraped_text = ' '.join(SCRAPE_RE.findall(text)) + matches = INNER_OPTIONS_RE.findall(scraped_text) + self._options[cmd] = matches + return {s for s in self._options[cmd] if s.startswith(prefix)} + + def _load_cached_options(self): + """Load options from file at startup.""" + try: + with open(OPTIONS_PATH, 'rb') as f: + self._options = pickle.load(f) + except: + self._options = {} + + def _save_cached_options(self): + """Save completions to file.""" + with open(OPTIONS_PATH, 'wb') as f: + pickle.dump(self._options, f) From 31b5af1949651c2393b9bcf2a4f4c78cbaa671ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaj=C4=85c?= Date: Mon, 25 May 2015 15:28:25 +0200 Subject: [PATCH 4/7] fix: ignoring man stderr --- xonsh/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xonsh/completer.py b/xonsh/completer.py index b8a422045..e6aadee2b 100644 --- a/xonsh/completer.py +++ b/xonsh/completer.py @@ -321,7 +321,7 @@ class ManCompleter(object): def option_complete(self, prefix, cmd): """Completes an option name, basing on content of man page.""" if cmd not in self._options.keys(): - manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE) + manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE, stderr=DEVNULL) # This is a trick to get rid of reverse line feeds text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') scraped_text = ' '.join(SCRAPE_RE.findall(text)) From 4ac3d9a8b5713dadf7e19485aba4368b52c181b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaj=C4=85c?= Date: Wed, 27 May 2015 17:11:26 +0200 Subject: [PATCH 5/7] stderr fix --- xonsh/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xonsh/completer.py b/xonsh/completer.py index e6aadee2b..f31badf15 100644 --- a/xonsh/completer.py +++ b/xonsh/completer.py @@ -321,7 +321,7 @@ class ManCompleter(object): def option_complete(self, prefix, cmd): """Completes an option name, basing on content of man page.""" if cmd not in self._options.keys(): - manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE, stderr=DEVNULL) + manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) # This is a trick to get rid of reverse line feeds text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') scraped_text = ' '.join(SCRAPE_RE.findall(text)) From e559e4c5a39d30c925efb372a29ff13ba3b71436 Mon Sep 17 00:00:00 2001 From: zajaczajac Date: Wed, 17 Jun 2015 22:58:05 +0200 Subject: [PATCH 6/7] tests for man completion --- tests/man1/yes.1.gz | Bin 0 -> 790 bytes tests/test_man.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/man1/yes.1.gz create mode 100644 tests/test_man.py diff --git a/tests/man1/yes.1.gz b/tests/man1/yes.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..8eea97b6d84508eca5df3b40ad2e85528c3dc417 GIT binary patch literal 790 zcmV+x1L^!9iwFP!0000218r2%ZsRr(eAicO{cXoq=2X7K7QCjv5s?ih#!|f0mKfD z+(Ls24R=_>%K&?n-BrsKT(av=EMmnPw#yu5mou3C&Q;IN09PL`Khbivd4@_G?46Jn zeq~qw%(F!Uce*8r+Rh@DuVJ%zT1Ep}C@(5Q-Sdu&8U` zY5-o>!U?Uogc@ySMCVu3x50>|X_UP&dL=7`LO2Sy2QDSp4lA(}q}Ryi%>H~1+Q5h5 zgBL6%F1O`HdEuq>Y-(XU$p>(IVS~QJqXzt2xc8?IXe<%&38%WJ2ne;)rsaX=TPC%* zcrMmS7JW6sIaL1k?)gE~@WiV>Q2is2g=4d9nI>65OyTxw!p+yCmIa@oHT}ce)#l zX!Z`uvr2noU&Dhzg#5${9(1qj0Col|CM>f4K==(Er!ly7pF}@-zk$ zqp2M^rrLSaNw#;b{5)E?@W!Cu&R2d@WY9f$3L#7lR(q}*3>Gj*rlJXY=^cV_j3CIr z9%GZh*F}~sHpTYbCy{_;7_x_h4o&Nw66Rd;_`TsI6V%M(W|oaMSX^kM?`nZrt4@7Z*Ay`7DR}qOepKP zA9~=<_iVrlhv$KLH9jN}i_;14ofpe9{=>ULsDrKoS04#eZ1=jCH8kRYqmEUD)=q*D UY$uVVJCTn51v$QVk|+fL07?jjs{jB1 literal 0 HcmV?d00001 diff --git a/tests/test_man.py b/tests/test_man.py new file mode 100644 index 000000000..ba92fa0ca --- /dev/null +++ b/tests/test_man.py @@ -0,0 +1,18 @@ +import os + +import nose +from nose.tools import assert_true + +from xonsh.completer import ManCompleter + +os.environ['MANPATH'] = os.path.dirname(os.path.abspath(__file__)) + +def test_man_completion(): + man_completer = ManCompleter() + completions = man_completer.option_complete('--', 'yes') + assert_true('--version' in completions) + assert_true('--help' in completions) + + +if __name__ == '__main__': + nose.runmodule() From 454c2140a6ed46f3fe7a043098b555ece7d1823c Mon Sep 17 00:00:00 2001 From: zajaczajac Date: Wed, 17 Jun 2015 23:23:15 +0200 Subject: [PATCH 7/7] man exec in try-catch block --- tests/test_man.py | 10 ++++++---- xonsh/completer.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/test_man.py b/tests/test_man.py index ba92fa0ca..2e82aa555 100644 --- a/tests/test_man.py +++ b/tests/test_man.py @@ -1,4 +1,5 @@ import os +import platform import nose from nose.tools import assert_true @@ -8,10 +9,11 @@ from xonsh.completer import ManCompleter os.environ['MANPATH'] = os.path.dirname(os.path.abspath(__file__)) def test_man_completion(): - man_completer = ManCompleter() - completions = man_completer.option_complete('--', 'yes') - assert_true('--version' in completions) - assert_true('--help' in completions) + if (platform.system() != 'Windows'): + man_completer = ManCompleter() + completions = man_completer.option_complete('--', 'yes') + assert_true('--version' in completions) + assert_true('--help' in completions) if __name__ == '__main__': diff --git a/xonsh/completer.py b/xonsh/completer.py index f31badf15..59ab20e0e 100644 --- a/xonsh/completer.py +++ b/xonsh/completer.py @@ -321,12 +321,15 @@ class ManCompleter(object): def option_complete(self, prefix, cmd): """Completes an option name, basing on content of man page.""" if cmd not in self._options.keys(): - manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - # This is a trick to get rid of reverse line feeds - text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') - scraped_text = ' '.join(SCRAPE_RE.findall(text)) - matches = INNER_OPTIONS_RE.findall(scraped_text) - self._options[cmd] = matches + try: + manpage = subprocess.Popen(["man", cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + # This is a trick to get rid of reverse line feeds + text = subprocess.check_output(["col", "-b"], stdin=manpage.stdout).decode('utf-8') + scraped_text = ' '.join(SCRAPE_RE.findall(text)) + matches = INNER_OPTIONS_RE.findall(scraped_text) + self._options[cmd] = matches + except: + return set() return {s for s in self._options[cmd] if s.startswith(prefix)} def _load_cached_options(self):