comparison misc/_jp @ 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
children 069ad98b360d
comparison
equal deleted inserted replaced
817:c39117d00f35 818:c328bcc4db71
1 #compdef jp jp_dev
2 # jp: a SAT command line tool
3 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
4
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
19 #TODO: - caching (see _store_cache en _retrieve_cache)
20 # - filtering imposibles arguments
21 # - arguments (jids, files)
22
23 PYTHON='python2'
24
25 local optionals subcommands arguments
26 local context state state_descr line
27 typeset -A val_args
28
29 _jp() {
30 eval `/usr/bin/env $PYTHON 2> /dev/null <<- PYTHONEND
31 import re
32 from subprocess import check_output
33
34 # import sys
35 # words_raw="jp_dev " + ' '.join(sys.argv[1:]) # for debugging in a script
36 words_raw="$words" # $words is the command line currently completed
37
38 words_all = words_raw.split()
39 prog_name = words_all[0]
40
41 words_no_opt = [word for word in words_all if not word.startswith('-')] # command line without optional arguments
42
43 choices_cache = {}
44
45 ARG = r'[-a-z0-9_]' # charset accepted for an argument name
46 subcommands_re = re.compile(r"^ +{((?:" + ARG + r"+)(?:," + ARG + r"+)*)}", re.MULTILINE)
47 optionals_re = re.compile(r"^ {2,}(--?" + ARG + r"+(?: [A-Z_0-9]+)?(?:, --" + ARG + r"+(?: [A-Z_0-9]+)?)?)\n? {2,}(.*(?:\n {4,}.*)*$)", re.MULTILINE)
48 arguments_re = re.compile(r"^ {2,}([a-z_]" + ARG + r"*) {2,}(.*$)", re.MULTILINE)
49 clean_re = re.compile(r"(?P<prefix_spaces>^ +)|(?P<double_spaces> {2,})|(?P<newline>\n)|(?P<quote>')|(?P<suffix_spaces> +$)", re.MULTILINE)
50
51 def _clean(desc):
52 def sub_clean(match):
53 matched_dict = match.groupdict()
54 matched = {matched for matched in matched_dict if matched_dict[matched]}
55 if matched.intersection(('prefix_spaces', 'suffix_spaces')):
56 return ''
57 elif matched.intersection(('double_spaces', 'newline')):
58 return ' '
59 elif matched.intersection(('quote',)):
60 return r"'\''"
61 else:
62 raise ValueError
63 return clean_re.sub(sub_clean, desc)
64
65 def parse_help(jp_help):
66 # parse the help returning subcommands, optionals arguments, and mandatory arguments
67 subcommands = subcommands_re.findall(jp_help)
68 subcommands = {subcommand:"" for subcommand in subcommands[0].split(',')} if subcommands else {}
69 optionals = dict(optionals_re.findall(jp_help))
70 arguments = dict(arguments_re.findall(jp_help))
71 for subcommand in subcommands:
72 subcommands[subcommand] = arguments.pop(subcommand, '')
73 return subcommands, optionals, arguments
74
75 def get_choice(opt_choice):
76 choices = choices_cache.get(opt_choice)
77 if choices is not None:
78 return choices
79 if opt_choice == 'PROFILE':
80 profiles = check_output([prog_name, 'profile', 'list'])
81 choices = ":profile:(%s)" % ' '.join(profiles.split('\n'))
82 if choices:
83 choices_cache[opt_choice] = choices
84 return choices
85 else:
86 return ""
87
88 def construct_opt(opts, desc):
89 # construct zsh's _arguments line for optional arguments
90 arg_lines = []
91 for opt in opts.split(', '):
92 try:
93 opt_name, opt_choice = opt.split()
94 except ValueError:
95 # there is no argument
96 opt_name, opt_choice = opt, None
97 # arg_lines.append("'()%s[%s]%s'" % (opt_name+('=' if opt_name.startswith('--') else '+'),
98 arg_lines.append("'()%s[%s]%s'" % (opt_name,
99 _clean(desc),
100 "%s" % get_choice(opt_choice) if opt_choice else ''
101 ))
102 return ' '.join(arg_lines)
103
104 current_args = []
105
106 while True:
107 # parse jp's help recursively until words_no_opt doesn't correspond anymore to a subcommand
108 try:
109 current_args.append(words_no_opt.pop(0))
110 jp_help = check_output(current_args + ['--help'])
111 # print "jp_help (%s):\n%s\n\n---\n" % (' '.join(current_args), jp_help) # for debugging
112 subcommands, optionals, arguments = parse_help(jp_help)
113 if words_no_opt[0] not in subcommands:
114 break
115 except IndexError:
116 break
117
118 # now we fill the arrays so zsh can use them
119 env=[]
120 env.append("optionals=(%s)" % ' '.join(construct_opt(opt, desc) for opt, desc in optionals.items()))
121 env.append("subcommands=(%s)" % ' '.join(["'%s[%s]'" % (subcommand, _clean(desc)) for subcommand, desc in subcommands.items()]))
122 env.append("arguments=(%s)" % ' '.join(["'%s[%s]'" % (argument, _clean(desc)) for argument, desc in arguments.items()]))
123
124 print ";".join(env) # this line is for eval
125 PYTHONEND
126 `
127
128 if [ -n "$optionals" ]; then
129 _values optional $optionals
130
131 fi
132 if [ -n "$subcommands" ]; then
133 _values subcommand $subcommands
134 fi
135 if [ -n "$arguments" ]; then
136 #_values argument $arguments
137 fi
138 }
139
140
141 _jp "$@"