changeset 818:c328bcc4db71

jp: zsh completion, first draft (added in a new /misc directory): - completion automatically parse jp --help for the subcommand being completed to find optional and mandatory arguments - profiles are asked to jp himself (through jp profile list) - complete jp and jp_dev
author Goffi <goffi@goffi.org>
date Mon, 10 Feb 2014 13:53:46 +0100
parents c39117d00f35
children 9e3641ea648f
files misc/README misc/_jp
diffstat 2 files changed, 153 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/README	Mon Feb 10 13:53:46 2014 +0100
@@ -0,0 +1,12 @@
+This directory contains files related to SàT but not directly used by it.
+
+* file _jp:
+    This is the completion file for zsh. To use it, you need to have it in a path accessible in your fpath variable, and to have completion activated. This can be done by the following commands in your .zshrc:
+
+### .zshrc completion ###
+fpath=(/path/to/directory/with/_jp/ $fpath)
+autoload -U compinit
+compinit
+### end of .zshrc completion ###
+
+    Then, you should be able to complete a jp command line by pressing [TAB].
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/_jp	Mon Feb 10 13:53:46 2014 +0100
@@ -0,0 +1,141 @@
+#compdef jp jp_dev
+# jp: a SAT command line tool
+# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+#TODO: - caching (see _store_cache en _retrieve_cache)
+#      - filtering imposibles arguments
+#      - arguments (jids, files)
+
+PYTHON='python2'
+
+local optionals subcommands arguments
+local context state state_descr line
+typeset -A val_args
+
+_jp() {
+	eval `/usr/bin/env $PYTHON 2> /dev/null <<- PYTHONEND
+	import re
+	from subprocess import check_output
+	
+	# import sys
+	# words_raw="jp_dev " + ' '.join(sys.argv[1:]) # for debugging in a script
+	words_raw="$words" # $words is the command line currently completed
+	
+	words_all = words_raw.split()
+	prog_name = words_all[0]
+	
+	words_no_opt = [word for word in words_all if not word.startswith('-')] # command line without optional arguments
+	
+	choices_cache = {}
+	
+	ARG = r'[-a-z0-9_]' # charset accepted for an argument name
+	subcommands_re = re.compile(r"^ +{((?:" + ARG + r"+)(?:," + ARG + r"+)*)}", re.MULTILINE)
+	optionals_re = re.compile(r"^ {2,}(--?" + ARG + r"+(?: [A-Z_0-9]+)?(?:, --" + ARG + r"+(?: [A-Z_0-9]+)?)?)\n? {2,}(.*(?:\n {4,}.*)*$)", re.MULTILINE)
+	arguments_re = re.compile(r"^ {2,}([a-z_]" + ARG + r"*) {2,}(.*$)", re.MULTILINE)
+	clean_re = re.compile(r"(?P<prefix_spaces>^ +)|(?P<double_spaces> {2,})|(?P<newline>\n)|(?P<quote>')|(?P<suffix_spaces> +$)", re.MULTILINE)
+	
+	def _clean(desc):
+	    def sub_clean(match):
+	        matched_dict = match.groupdict()
+	        matched = {matched for matched in matched_dict if matched_dict[matched]}
+	        if matched.intersection(('prefix_spaces', 'suffix_spaces')):
+	            return ''
+	        elif matched.intersection(('double_spaces', 'newline')):
+	            return ' '
+	        elif matched.intersection(('quote',)):
+	            return r"'\''"
+	        else:
+	            raise ValueError
+	    return clean_re.sub(sub_clean, desc)
+	
+	def parse_help(jp_help):
+	    # parse the help returning subcommands, optionals arguments, and mandatory arguments
+	    subcommands = subcommands_re.findall(jp_help)
+	    subcommands = {subcommand:"" for subcommand in subcommands[0].split(',')} if subcommands else {}
+	    optionals = dict(optionals_re.findall(jp_help))
+	    arguments = dict(arguments_re.findall(jp_help))
+	    for subcommand in subcommands:
+	        subcommands[subcommand] = arguments.pop(subcommand, '')
+	    return subcommands, optionals, arguments
+	
+	def get_choice(opt_choice):
+	    choices = choices_cache.get(opt_choice)
+	    if choices is not None:
+	        return choices
+	    if opt_choice == 'PROFILE':
+	        profiles = check_output([prog_name, 'profile', 'list'])
+	        choices = ":profile:(%s)" % ' '.join(profiles.split('\n'))
+	    if choices:
+	        choices_cache[opt_choice] = choices
+	        return choices
+	    else:
+	        return ""
+	
+	def construct_opt(opts, desc):
+	    # construct zsh's _arguments line for optional arguments
+	    arg_lines = []
+	    for opt in opts.split(', '):
+	        try:
+	            opt_name, opt_choice = opt.split()
+	        except ValueError:
+	            # there is no argument
+	            opt_name, opt_choice = opt, None
+	        # arg_lines.append("'()%s[%s]%s'" % (opt_name+('=' if opt_name.startswith('--') else '+'),
+	        arg_lines.append("'()%s[%s]%s'" % (opt_name,
+	                                           _clean(desc),
+	                                           "%s" % get_choice(opt_choice) if opt_choice else ''
+	                                          ))
+	    return ' '.join(arg_lines)
+	
+	current_args = []
+	
+	while True:
+	    # parse jp's help recursively until words_no_opt doesn't correspond anymore to a subcommand
+	    try:
+	        current_args.append(words_no_opt.pop(0))
+	        jp_help = check_output(current_args + ['--help'])
+	        # print "jp_help (%s):\n%s\n\n---\n" % (' '.join(current_args), jp_help) # for debugging
+	        subcommands, optionals, arguments = parse_help(jp_help)
+	        if words_no_opt[0] not in subcommands:
+	            break
+	    except IndexError:
+	        break
+	
+	# now we fill the arrays so zsh can use them
+	env=[]
+	env.append("optionals=(%s)" % ' '.join(construct_opt(opt, desc) for opt, desc in optionals.items()))
+	env.append("subcommands=(%s)" % ' '.join(["'%s[%s]'" % (subcommand, _clean(desc)) for subcommand, desc in subcommands.items()]))
+	env.append("arguments=(%s)" % ' '.join(["'%s[%s]'" % (argument, _clean(desc)) for argument, desc in arguments.items()]))
+	
+	print ";".join(env) # this line is for eval
+	PYTHONEND
+	`
+	
+	if [  -n "$optionals" ]; then
+		_values optional $optionals
+		
+	fi
+	if [  -n "$subcommands" ]; then
+		_values subcommand $subcommands
+	fi
+	if [ -n "$arguments" ]; then
+		#_values argument $arguments
+	fi
+}
+
+
+_jp "$@"