##// END OF EJS Templates
fancyopts: prevent mutation of the default value in customopts...
Daniel Ploch -
r37110:ef6215df default
parent child Browse files
Show More
@@ -1,368 +1,378
1 1 # fancyopts.py - better command line parsing
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import abc
11 11 import functools
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 )
18 18
19 19 # Set of flags to not apply boolean negation logic on
20 20 nevernegate = {
21 21 # avoid --no-noninteractive
22 22 'noninteractive',
23 23 # These two flags are special because they cause hg to do one
24 24 # thing and then exit, and so aren't suitable for use in things
25 25 # like aliases anyway.
26 26 'help',
27 27 'version',
28 28 }
29 29
30 30 def _earlyoptarg(arg, shortlist, namelist):
31 31 """Check if the given arg is a valid unabbreviated option
32 32
33 33 Returns (flag_str, has_embedded_value?, embedded_value, takes_value?)
34 34
35 35 >>> def opt(arg):
36 36 ... return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger'])
37 37
38 38 long form:
39 39
40 40 >>> opt(b'--cwd')
41 41 ('--cwd', False, '', True)
42 42 >>> opt(b'--cwd=')
43 43 ('--cwd', True, '', True)
44 44 >>> opt(b'--cwd=foo')
45 45 ('--cwd', True, 'foo', True)
46 46 >>> opt(b'--debugger')
47 47 ('--debugger', False, '', False)
48 48 >>> opt(b'--debugger=') # invalid but parsable
49 49 ('--debugger', True, '', False)
50 50
51 51 short form:
52 52
53 53 >>> opt(b'-R')
54 54 ('-R', False, '', True)
55 55 >>> opt(b'-Rfoo')
56 56 ('-R', True, 'foo', True)
57 57 >>> opt(b'-q')
58 58 ('-q', False, '', False)
59 59 >>> opt(b'-qfoo') # invalid but parsable
60 60 ('-q', True, 'foo', False)
61 61
62 62 unknown or invalid:
63 63
64 64 >>> opt(b'--unknown')
65 65 ('', False, '', False)
66 66 >>> opt(b'-u')
67 67 ('', False, '', False)
68 68 >>> opt(b'-ufoo')
69 69 ('', False, '', False)
70 70 >>> opt(b'--')
71 71 ('', False, '', False)
72 72 >>> opt(b'-')
73 73 ('', False, '', False)
74 74 >>> opt(b'-:')
75 75 ('', False, '', False)
76 76 >>> opt(b'-:foo')
77 77 ('', False, '', False)
78 78 """
79 79 if arg.startswith('--'):
80 80 flag, eq, val = arg.partition('=')
81 81 if flag[2:] in namelist:
82 82 return flag, bool(eq), val, False
83 83 if flag[2:] + '=' in namelist:
84 84 return flag, bool(eq), val, True
85 85 elif arg.startswith('-') and arg != '-' and not arg.startswith('-:'):
86 86 flag, val = arg[:2], arg[2:]
87 87 i = shortlist.find(flag[1:])
88 88 if i >= 0:
89 89 return flag, bool(val), val, shortlist.startswith(':', i + 1)
90 90 return '', False, '', False
91 91
92 92 def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False):
93 93 """Parse options like getopt, but ignores unknown options and abbreviated
94 94 forms
95 95
96 96 If gnu=False, this stops processing options as soon as a non/unknown-option
97 97 argument is encountered. Otherwise, option and non-option arguments may be
98 98 intermixed, and unknown-option arguments are taken as non-option.
99 99
100 100 If keepsep=True, '--' won't be removed from the list of arguments left.
101 101 This is useful for stripping early options from a full command arguments.
102 102
103 103 >>> def get(args, gnu=False, keepsep=False):
104 104 ... return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'],
105 105 ... gnu=gnu, keepsep=keepsep)
106 106
107 107 default parsing rules for early options:
108 108
109 109 >>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True)
110 110 ([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y'])
111 111 >>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True)
112 112 ([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y'])
113 113 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True)
114 114 ([('--cwd', 'foo')], ['--unknown', '--debugger'])
115 115
116 116 restricted parsing rules (early options must come first):
117 117
118 118 >>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False)
119 119 ([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y'])
120 120 >>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False)
121 121 ([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger'])
122 122 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False)
123 123 ([], ['--unknown', '--cwd=foo', '--', '--debugger'])
124 124
125 125 stripping early options (without loosing '--'):
126 126
127 127 >>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1]
128 128 ['x', '--', '--debugger']
129 129
130 130 last argument:
131 131
132 132 >>> get([b'--cwd'])
133 133 ([], ['--cwd'])
134 134 >>> get([b'--cwd=foo'])
135 135 ([('--cwd', 'foo')], [])
136 136 >>> get([b'-R'])
137 137 ([], ['-R'])
138 138 >>> get([b'-Rbar'])
139 139 ([('-R', 'bar')], [])
140 140 >>> get([b'-q'])
141 141 ([('-q', '')], [])
142 142 >>> get([b'-q', b'--'])
143 143 ([('-q', '')], [])
144 144
145 145 '--' may be a value:
146 146
147 147 >>> get([b'-R', b'--', b'x'])
148 148 ([('-R', '--')], ['x'])
149 149 >>> get([b'--cwd', b'--', b'x'])
150 150 ([('--cwd', '--')], ['x'])
151 151
152 152 value passed to bool options:
153 153
154 154 >>> get([b'--debugger=foo', b'x'])
155 155 ([], ['--debugger=foo', 'x'])
156 156 >>> get([b'-qfoo', b'x'])
157 157 ([], ['-qfoo', 'x'])
158 158
159 159 short option isn't separated with '=':
160 160
161 161 >>> get([b'-R=bar'])
162 162 ([('-R', '=bar')], [])
163 163
164 164 ':' may be in shortlist, but shouldn't be taken as an option letter:
165 165
166 166 >>> get([b'-:', b'y'])
167 167 ([], ['-:', 'y'])
168 168
169 169 '-' is a valid non-option argument:
170 170
171 171 >>> get([b'-', b'y'])
172 172 ([], ['-', 'y'])
173 173 """
174 174 parsedopts = []
175 175 parsedargs = []
176 176 pos = 0
177 177 while pos < len(args):
178 178 arg = args[pos]
179 179 if arg == '--':
180 180 pos += not keepsep
181 181 break
182 182 flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist)
183 183 if not hasval and takeval and pos + 1 >= len(args):
184 184 # missing last argument
185 185 break
186 186 if not flag or hasval and not takeval:
187 187 # non-option argument or -b/--bool=INVALID_VALUE
188 188 if gnu:
189 189 parsedargs.append(arg)
190 190 pos += 1
191 191 else:
192 192 break
193 193 elif hasval == takeval:
194 194 # -b/--bool or -s/--str=VALUE
195 195 parsedopts.append((flag, val))
196 196 pos += 1
197 197 else:
198 198 # -s/--str VALUE
199 199 parsedopts.append((flag, args[pos + 1]))
200 200 pos += 2
201 201
202 202 parsedargs.extend(args[pos:])
203 203 return parsedopts, parsedargs
204 204
205 205 class customopt(object):
206 206 """Manage defaults and mutations for any type of opt."""
207 207
208 208 __metaclass__ = abc.ABCMeta
209 209
210 210 def __init__(self, defaultvalue):
211 self.defaultvalue = defaultvalue
211 self._defaultvalue = defaultvalue
212 212
213 213 def _isboolopt(self):
214 214 return False
215 215
216 def getdefaultvalue(self):
217 """Returns the default value for this opt.
218
219 Subclasses should override this to return a new value if the value type
220 is mutable."""
221 return self._defaultvalue
222
216 223 @abc.abstractmethod
217 224 def newstate(self, oldstate, newparam, abort):
218 225 """Adds newparam to oldstate and returns the new state.
219 226
220 227 On failure, abort can be called with a string error message."""
221 228
222 229 class _simpleopt(customopt):
223 230 def _isboolopt(self):
224 return isinstance(self.defaultvalue, (bool, type(None)))
231 return isinstance(self._defaultvalue, (bool, type(None)))
225 232
226 233 def newstate(self, oldstate, newparam, abort):
227 234 return newparam
228 235
229 236 class _callableopt(customopt):
230 237 def __init__(self, callablefn):
231 238 self.callablefn = callablefn
232 239 super(_callableopt, self).__init__(None)
233 240
234 241 def newstate(self, oldstate, newparam, abort):
235 242 return self.callablefn(newparam)
236 243
237 244 class _listopt(customopt):
245 def getdefaultvalue(self):
246 return self._defaultvalue[:]
247
238 248 def newstate(self, oldstate, newparam, abort):
239 249 oldstate.append(newparam)
240 250 return oldstate
241 251
242 252 class _intopt(customopt):
243 253 def newstate(self, oldstate, newparam, abort):
244 254 try:
245 255 return int(newparam)
246 256 except ValueError:
247 257 abort(_('expected int'))
248 258
249 259 def _defaultopt(default):
250 260 """Returns a default opt implementation, given a default value."""
251 261
252 262 if isinstance(default, customopt):
253 263 return default
254 264 elif callable(default):
255 265 return _callableopt(default)
256 266 elif isinstance(default, list):
257 267 return _listopt(default[:])
258 268 elif type(default) is type(1):
259 269 return _intopt(default)
260 270 else:
261 271 return _simpleopt(default)
262 272
263 273 def fancyopts(args, options, state, gnu=False, early=False, optaliases=None):
264 274 """
265 275 read args, parse options, and store options in state
266 276
267 277 each option is a tuple of:
268 278
269 279 short option or ''
270 280 long option
271 281 default value
272 282 description
273 283 option value label(optional)
274 284
275 285 option types include:
276 286
277 287 boolean or none - option sets variable in state to true
278 288 string - parameter string is stored in state
279 289 list - parameter string is added to a list
280 290 integer - parameter strings is stored as int
281 291 function - call function with parameter
282 292 customopt - subclass of 'customopt'
283 293
284 294 optaliases is a mapping from a canonical option name to a list of
285 295 additional long options. This exists for preserving backward compatibility
286 296 of early options. If we want to use it extensively, please consider moving
287 297 the functionality to the options table (e.g separate long options by '|'.)
288 298
289 299 non-option args are returned
290 300 """
291 301 if optaliases is None:
292 302 optaliases = {}
293 303 namelist = []
294 304 shortlist = ''
295 305 argmap = {}
296 306 defmap = {}
297 307 negations = {}
298 308 alllong = set(o[1] for o in options)
299 309
300 310 for option in options:
301 311 if len(option) == 5:
302 312 short, name, default, comment, dummy = option
303 313 else:
304 314 short, name, default, comment = option
305 315 # convert opts to getopt format
306 316 onames = [name]
307 317 onames.extend(optaliases.get(name, []))
308 318 name = name.replace('-', '_')
309 319
310 320 argmap['-' + short] = name
311 321 for n in onames:
312 322 argmap['--' + n] = name
313 323 defmap[name] = _defaultopt(default)
314 324
315 325 # copy defaults to state
316 state[name] = defmap[name].defaultvalue
326 state[name] = defmap[name].getdefaultvalue()
317 327
318 328 # does it take a parameter?
319 329 if not defmap[name]._isboolopt():
320 330 if short:
321 331 short += ':'
322 332 onames = [n + '=' for n in onames]
323 333 elif name not in nevernegate:
324 334 for n in onames:
325 335 if n.startswith('no-'):
326 336 insert = n[3:]
327 337 else:
328 338 insert = 'no-' + n
329 339 # backout (as a practical example) has both --commit and
330 340 # --no-commit options, so we don't want to allow the
331 341 # negations of those flags.
332 342 if insert not in alllong:
333 343 assert ('--' + n) not in negations
334 344 negations['--' + insert] = '--' + n
335 345 namelist.append(insert)
336 346 if short:
337 347 shortlist += short
338 348 if name:
339 349 namelist.extend(onames)
340 350
341 351 # parse arguments
342 352 if early:
343 353 parse = functools.partial(earlygetopt, gnu=gnu)
344 354 elif gnu:
345 355 parse = pycompat.gnugetoptb
346 356 else:
347 357 parse = pycompat.getoptb
348 358 opts, args = parse(args, shortlist, namelist)
349 359
350 360 # transfer result to state
351 361 for opt, val in opts:
352 362 boolval = True
353 363 negation = negations.get(opt, False)
354 364 if negation:
355 365 opt = negation
356 366 boolval = False
357 367 name = argmap[opt]
358 368 obj = defmap[name]
359 369 if obj._isboolopt():
360 370 state[name] = boolval
361 371 else:
362 372 def abort(s):
363 373 raise error.Abort(
364 374 _('invalid value %r for option %s, %s') % (val, opt, s))
365 375 state[name] = defmap[name].newstate(state[name], val, abort)
366 376
367 377 # return unparsed args
368 378 return args
@@ -1,689 +1,689
1 1 # help.py - help data for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12 import textwrap
13 13
14 14 from .i18n import (
15 15 _,
16 16 gettext,
17 17 )
18 18 from . import (
19 19 cmdutil,
20 20 encoding,
21 21 error,
22 22 extensions,
23 23 fancyopts,
24 24 filemerge,
25 25 fileset,
26 26 minirst,
27 27 pycompat,
28 28 revset,
29 29 templatefilters,
30 30 templatefuncs,
31 31 templatekw,
32 32 util,
33 33 )
34 34 from .hgweb import (
35 35 webcommands,
36 36 )
37 37
38 38 _exclkeywords = {
39 39 "(ADVANCED)",
40 40 "(DEPRECATED)",
41 41 "(EXPERIMENTAL)",
42 42 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
43 43 _("(ADVANCED)"),
44 44 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
45 45 _("(DEPRECATED)"),
46 46 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
47 47 _("(EXPERIMENTAL)"),
48 48 }
49 49
50 50 def listexts(header, exts, indent=1, showdeprecated=False):
51 51 '''return a text listing of the given extensions'''
52 52 rst = []
53 53 if exts:
54 54 for name, desc in sorted(exts.iteritems()):
55 55 if not showdeprecated and any(w in desc for w in _exclkeywords):
56 56 continue
57 57 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
58 58 if rst:
59 59 rst.insert(0, '\n%s\n\n' % header)
60 60 return rst
61 61
62 62 def extshelp(ui):
63 63 rst = loaddoc('extensions')(ui).splitlines(True)
64 64 rst.extend(listexts(
65 65 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
66 66 rst.extend(listexts(_('disabled extensions:'), extensions.disabled(),
67 67 showdeprecated=ui.verbose))
68 68 doc = ''.join(rst)
69 69 return doc
70 70
71 71 def optrst(header, options, verbose):
72 72 data = []
73 73 multioccur = False
74 74 for option in options:
75 75 if len(option) == 5:
76 76 shortopt, longopt, default, desc, optlabel = option
77 77 else:
78 78 shortopt, longopt, default, desc = option
79 79 optlabel = _("VALUE") # default label
80 80
81 81 if not verbose and any(w in desc for w in _exclkeywords):
82 82 continue
83 83
84 84 so = ''
85 85 if shortopt:
86 86 so = '-' + shortopt
87 87 lo = '--' + longopt
88 88
89 89 if isinstance(default, fancyopts.customopt):
90 default = default.defaultvalue
90 default = default.getdefaultvalue()
91 91 if default and not callable(default):
92 92 # default is of unknown type, and in Python 2 we abused
93 93 # the %s-shows-repr property to handle integers etc. To
94 94 # match that behavior on Python 3, we do str(default) and
95 95 # then convert it to bytes.
96 96 desc += _(" (default: %s)") % pycompat.bytestr(default)
97 97
98 98 if isinstance(default, list):
99 99 lo += " %s [+]" % optlabel
100 100 multioccur = True
101 101 elif (default is not None) and not isinstance(default, bool):
102 102 lo += " %s" % optlabel
103 103
104 104 data.append((so, lo, desc))
105 105
106 106 if multioccur:
107 107 header += (_(" ([+] can be repeated)"))
108 108
109 109 rst = ['\n%s:\n\n' % header]
110 110 rst.extend(minirst.maketable(data, 1))
111 111
112 112 return ''.join(rst)
113 113
114 114 def indicateomitted(rst, omitted, notomitted=None):
115 115 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
116 116 if notomitted:
117 117 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
118 118
119 119 def filtercmd(ui, cmd, kw, doc):
120 120 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
121 121 return True
122 122 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
123 123 return True
124 124 return False
125 125
126 126 def topicmatch(ui, commands, kw):
127 127 """Return help topics matching kw.
128 128
129 129 Returns {'section': [(name, summary), ...], ...} where section is
130 130 one of topics, commands, extensions, or extensioncommands.
131 131 """
132 132 kw = encoding.lower(kw)
133 133 def lowercontains(container):
134 134 return kw in encoding.lower(container) # translated in helptable
135 135 results = {'topics': [],
136 136 'commands': [],
137 137 'extensions': [],
138 138 'extensioncommands': [],
139 139 }
140 140 for names, header, doc in helptable:
141 141 # Old extensions may use a str as doc.
142 142 if (sum(map(lowercontains, names))
143 143 or lowercontains(header)
144 144 or (callable(doc) and lowercontains(doc(ui)))):
145 145 results['topics'].append((names[0], header))
146 146 for cmd, entry in commands.table.iteritems():
147 147 if len(entry) == 3:
148 148 summary = entry[2]
149 149 else:
150 150 summary = ''
151 151 # translate docs *before* searching there
152 152 docs = _(pycompat.getdoc(entry[0])) or ''
153 153 if kw in cmd or lowercontains(summary) or lowercontains(docs):
154 154 doclines = docs.splitlines()
155 155 if doclines:
156 156 summary = doclines[0]
157 157 cmdname = cmdutil.parsealiases(cmd)[0]
158 158 if filtercmd(ui, cmdname, kw, docs):
159 159 continue
160 160 results['commands'].append((cmdname, summary))
161 161 for name, docs in itertools.chain(
162 162 extensions.enabled(False).iteritems(),
163 163 extensions.disabled().iteritems()):
164 164 if not docs:
165 165 continue
166 166 name = name.rpartition('.')[-1]
167 167 if lowercontains(name) or lowercontains(docs):
168 168 # extension docs are already translated
169 169 results['extensions'].append((name, docs.splitlines()[0]))
170 170 try:
171 171 mod = extensions.load(ui, name, '')
172 172 except ImportError:
173 173 # debug message would be printed in extensions.load()
174 174 continue
175 175 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
176 176 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
177 177 cmdname = cmdutil.parsealiases(cmd)[0]
178 178 cmddoc = pycompat.getdoc(entry[0])
179 179 if cmddoc:
180 180 cmddoc = gettext(cmddoc).splitlines()[0]
181 181 else:
182 182 cmddoc = _('(no help text available)')
183 183 if filtercmd(ui, cmdname, kw, cmddoc):
184 184 continue
185 185 results['extensioncommands'].append((cmdname, cmddoc))
186 186 return results
187 187
188 188 def loaddoc(topic, subdir=None):
189 189 """Return a delayed loader for help/topic.txt."""
190 190
191 191 def loader(ui):
192 192 docdir = os.path.join(util.datapath, 'help')
193 193 if subdir:
194 194 docdir = os.path.join(docdir, subdir)
195 195 path = os.path.join(docdir, topic + ".txt")
196 196 doc = gettext(util.readfile(path))
197 197 for rewriter in helphooks.get(topic, []):
198 198 doc = rewriter(ui, topic, doc)
199 199 return doc
200 200
201 201 return loader
202 202
203 203 internalstable = sorted([
204 204 (['bundle2'], _('Bundle2'),
205 205 loaddoc('bundle2', subdir='internals')),
206 206 (['bundles'], _('Bundles'),
207 207 loaddoc('bundles', subdir='internals')),
208 208 (['censor'], _('Censor'),
209 209 loaddoc('censor', subdir='internals')),
210 210 (['changegroups'], _('Changegroups'),
211 211 loaddoc('changegroups', subdir='internals')),
212 212 (['config'], _('Config Registrar'),
213 213 loaddoc('config', subdir='internals')),
214 214 (['requirements'], _('Repository Requirements'),
215 215 loaddoc('requirements', subdir='internals')),
216 216 (['revlogs'], _('Revision Logs'),
217 217 loaddoc('revlogs', subdir='internals')),
218 218 (['wireprotocol'], _('Wire Protocol'),
219 219 loaddoc('wireprotocol', subdir='internals')),
220 220 ])
221 221
222 222 def internalshelp(ui):
223 223 """Generate the index for the "internals" topic."""
224 224 lines = ['To access a subtopic, use "hg help internals.{subtopic-name}"\n',
225 225 '\n']
226 226 for names, header, doc in internalstable:
227 227 lines.append(' :%s: %s\n' % (names[0], header))
228 228
229 229 return ''.join(lines)
230 230
231 231 helptable = sorted([
232 232 (['bundlespec'], _("Bundle File Formats"), loaddoc('bundlespec')),
233 233 (['color'], _("Colorizing Outputs"), loaddoc('color')),
234 234 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
235 235 (["dates"], _("Date Formats"), loaddoc('dates')),
236 236 (["flags"], _("Command-line flags"), loaddoc('flags')),
237 237 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
238 238 (['environment', 'env'], _('Environment Variables'),
239 239 loaddoc('environment')),
240 240 (['revisions', 'revs', 'revsets', 'revset', 'multirevs', 'mrevs'],
241 241 _('Specifying Revisions'), loaddoc('revisions')),
242 242 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
243 243 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
244 244 (['merge-tools', 'mergetools', 'mergetool'], _('Merge Tools'),
245 245 loaddoc('merge-tools')),
246 246 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
247 247 loaddoc('templates')),
248 248 (['urls'], _('URL Paths'), loaddoc('urls')),
249 249 (["extensions"], _("Using Additional Features"), extshelp),
250 250 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
251 251 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
252 252 (["glossary"], _("Glossary"), loaddoc('glossary')),
253 253 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
254 254 loaddoc('hgignore')),
255 255 (["phases"], _("Working with Phases"), loaddoc('phases')),
256 256 (['scripting'], _('Using Mercurial from scripts and automation'),
257 257 loaddoc('scripting')),
258 258 (['internals'], _("Technical implementation topics"),
259 259 internalshelp),
260 260 (['pager'], _("Pager Support"), loaddoc('pager')),
261 261 ])
262 262
263 263 # Maps topics with sub-topics to a list of their sub-topics.
264 264 subtopics = {
265 265 'internals': internalstable,
266 266 }
267 267
268 268 # Map topics to lists of callable taking the current topic help and
269 269 # returning the updated version
270 270 helphooks = {}
271 271
272 272 def addtopichook(topic, rewriter):
273 273 helphooks.setdefault(topic, []).append(rewriter)
274 274
275 275 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
276 276 """Extract docstring from the items key to function mapping, build a
277 277 single documentation block and use it to overwrite the marker in doc.
278 278 """
279 279 entries = []
280 280 for name in sorted(items):
281 281 text = (pycompat.getdoc(items[name]) or '').rstrip()
282 282 if (not text
283 283 or not ui.verbose and any(w in text for w in _exclkeywords)):
284 284 continue
285 285 text = gettext(text)
286 286 if dedent:
287 287 # Abuse latin1 to use textwrap.dedent() on bytes.
288 288 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
289 289 lines = text.splitlines()
290 290 doclines = [(lines[0])]
291 291 for l in lines[1:]:
292 292 # Stop once we find some Python doctest
293 293 if l.strip().startswith('>>>'):
294 294 break
295 295 if dedent:
296 296 doclines.append(l.rstrip())
297 297 else:
298 298 doclines.append(' ' + l.strip())
299 299 entries.append('\n'.join(doclines))
300 300 entries = '\n\n'.join(entries)
301 301 return doc.replace(marker, entries)
302 302
303 303 def addtopicsymbols(topic, marker, symbols, dedent=False):
304 304 def add(ui, topic, doc):
305 305 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
306 306 addtopichook(topic, add)
307 307
308 308 addtopicsymbols('bundlespec', '.. bundlecompressionmarker',
309 309 util.bundlecompressiontopics())
310 310 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
311 311 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
312 312 filemerge.internalsdoc)
313 313 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
314 314 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
315 315 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
316 316 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
317 317 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
318 318 dedent=True)
319 319
320 320 def help_(ui, commands, name, unknowncmd=False, full=True, subtopic=None,
321 321 **opts):
322 322 '''
323 323 Generate the help for 'name' as unformatted restructured text. If
324 324 'name' is None, describe the commands available.
325 325 '''
326 326
327 327 opts = pycompat.byteskwargs(opts)
328 328
329 329 def helpcmd(name, subtopic=None):
330 330 try:
331 331 aliases, entry = cmdutil.findcmd(name, commands.table,
332 332 strict=unknowncmd)
333 333 except error.AmbiguousCommand as inst:
334 334 # py3k fix: except vars can't be used outside the scope of the
335 335 # except block, nor can be used inside a lambda. python issue4617
336 336 prefix = inst.args[0]
337 337 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
338 338 rst = helplist(select)
339 339 return rst
340 340
341 341 rst = []
342 342
343 343 # check if it's an invalid alias and display its error if it is
344 344 if getattr(entry[0], 'badalias', None):
345 345 rst.append(entry[0].badalias + '\n')
346 346 if entry[0].unknowncmd:
347 347 try:
348 348 rst.extend(helpextcmd(entry[0].cmdname))
349 349 except error.UnknownCommand:
350 350 pass
351 351 return rst
352 352
353 353 # synopsis
354 354 if len(entry) > 2:
355 355 if entry[2].startswith('hg'):
356 356 rst.append("%s\n" % entry[2])
357 357 else:
358 358 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
359 359 else:
360 360 rst.append('hg %s\n' % aliases[0])
361 361 # aliases
362 362 if full and not ui.quiet and len(aliases) > 1:
363 363 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
364 364 rst.append('\n')
365 365
366 366 # description
367 367 doc = gettext(pycompat.getdoc(entry[0]))
368 368 if not doc:
369 369 doc = _("(no help text available)")
370 370 if util.safehasattr(entry[0], 'definition'): # aliased command
371 371 source = entry[0].source
372 372 if entry[0].definition.startswith('!'): # shell alias
373 373 doc = (_('shell alias for::\n\n %s\n\ndefined by: %s\n') %
374 374 (entry[0].definition[1:], source))
375 375 else:
376 376 doc = (_('alias for: hg %s\n\n%s\n\ndefined by: %s\n') %
377 377 (entry[0].definition, doc, source))
378 378 doc = doc.splitlines(True)
379 379 if ui.quiet or not full:
380 380 rst.append(doc[0])
381 381 else:
382 382 rst.extend(doc)
383 383 rst.append('\n')
384 384
385 385 # check if this command shadows a non-trivial (multi-line)
386 386 # extension help text
387 387 try:
388 388 mod = extensions.find(name)
389 389 doc = gettext(pycompat.getdoc(mod)) or ''
390 390 if '\n' in doc.strip():
391 391 msg = _("(use 'hg help -e %s' to show help for "
392 392 "the %s extension)") % (name, name)
393 393 rst.append('\n%s\n' % msg)
394 394 except KeyError:
395 395 pass
396 396
397 397 # options
398 398 if not ui.quiet and entry[1]:
399 399 rst.append(optrst(_("options"), entry[1], ui.verbose))
400 400
401 401 if ui.verbose:
402 402 rst.append(optrst(_("global options"),
403 403 commands.globalopts, ui.verbose))
404 404
405 405 if not ui.verbose:
406 406 if not full:
407 407 rst.append(_("\n(use 'hg %s -h' to show more help)\n")
408 408 % name)
409 409 elif not ui.quiet:
410 410 rst.append(_('\n(some details hidden, use --verbose '
411 411 'to show complete help)'))
412 412
413 413 return rst
414 414
415 415
416 416 def helplist(select=None, **opts):
417 417 # list of commands
418 418 if name == "shortlist":
419 419 header = _('basic commands:\n\n')
420 420 elif name == "debug":
421 421 header = _('debug commands (internal and unsupported):\n\n')
422 422 else:
423 423 header = _('list of commands:\n\n')
424 424
425 425 h = {}
426 426 cmds = {}
427 427 for c, e in commands.table.iteritems():
428 428 fs = cmdutil.parsealiases(c)
429 429 f = fs[0]
430 430 p = ''
431 431 if c.startswith("^"):
432 432 p = '^'
433 433 if select and not select(p + f):
434 434 continue
435 435 if (not select and name != 'shortlist' and
436 436 e[0].__module__ != commands.__name__):
437 437 continue
438 438 if name == "shortlist" and not p:
439 439 continue
440 440 doc = pycompat.getdoc(e[0])
441 441 if filtercmd(ui, f, name, doc):
442 442 continue
443 443 doc = gettext(doc)
444 444 if not doc:
445 445 doc = _("(no help text available)")
446 446 h[f] = doc.splitlines()[0].rstrip()
447 447 cmds[f] = '|'.join(fs)
448 448
449 449 rst = []
450 450 if not h:
451 451 if not ui.quiet:
452 452 rst.append(_('no commands defined\n'))
453 453 return rst
454 454
455 455 if not ui.quiet:
456 456 rst.append(header)
457 457 fns = sorted(h)
458 458 for f in fns:
459 459 if ui.verbose:
460 460 commacmds = cmds[f].replace("|",", ")
461 461 rst.append(" :%s: %s\n" % (commacmds, h[f]))
462 462 else:
463 463 rst.append(' :%s: %s\n' % (f, h[f]))
464 464
465 465 ex = opts.get
466 466 anyopts = (ex(r'keyword') or not (ex(r'command') or ex(r'extension')))
467 467 if not name and anyopts:
468 468 exts = listexts(_('enabled extensions:'), extensions.enabled())
469 469 if exts:
470 470 rst.append('\n')
471 471 rst.extend(exts)
472 472
473 473 rst.append(_("\nadditional help topics:\n\n"))
474 474 topics = []
475 475 for names, header, doc in helptable:
476 476 topics.append((names[0], header))
477 477 for t, desc in topics:
478 478 rst.append(" :%s: %s\n" % (t, desc))
479 479
480 480 if ui.quiet:
481 481 pass
482 482 elif ui.verbose:
483 483 rst.append('\n%s\n' % optrst(_("global options"),
484 484 commands.globalopts, ui.verbose))
485 485 if name == 'shortlist':
486 486 rst.append(_("\n(use 'hg help' for the full list "
487 487 "of commands)\n"))
488 488 else:
489 489 if name == 'shortlist':
490 490 rst.append(_("\n(use 'hg help' for the full list of commands "
491 491 "or 'hg -v' for details)\n"))
492 492 elif name and not full:
493 493 rst.append(_("\n(use 'hg help %s' to show the full help "
494 494 "text)\n") % name)
495 495 elif name and cmds and name in cmds.keys():
496 496 rst.append(_("\n(use 'hg help -v -e %s' to show built-in "
497 497 "aliases and global options)\n") % name)
498 498 else:
499 499 rst.append(_("\n(use 'hg help -v%s' to show built-in aliases "
500 500 "and global options)\n")
501 501 % (name and " " + name or ""))
502 502 return rst
503 503
504 504 def helptopic(name, subtopic=None):
505 505 # Look for sub-topic entry first.
506 506 header, doc = None, None
507 507 if subtopic and name in subtopics:
508 508 for names, header, doc in subtopics[name]:
509 509 if subtopic in names:
510 510 break
511 511
512 512 if not header:
513 513 for names, header, doc in helptable:
514 514 if name in names:
515 515 break
516 516 else:
517 517 raise error.UnknownCommand(name)
518 518
519 519 rst = [minirst.section(header)]
520 520
521 521 # description
522 522 if not doc:
523 523 rst.append(" %s\n" % _("(no help text available)"))
524 524 if callable(doc):
525 525 rst += [" %s\n" % l for l in doc(ui).splitlines()]
526 526
527 527 if not ui.verbose:
528 528 omitted = _('(some details hidden, use --verbose'
529 529 ' to show complete help)')
530 530 indicateomitted(rst, omitted)
531 531
532 532 try:
533 533 cmdutil.findcmd(name, commands.table)
534 534 rst.append(_("\nuse 'hg help -c %s' to see help for "
535 535 "the %s command\n") % (name, name))
536 536 except error.UnknownCommand:
537 537 pass
538 538 return rst
539 539
540 540 def helpext(name, subtopic=None):
541 541 try:
542 542 mod = extensions.find(name)
543 543 doc = gettext(pycompat.getdoc(mod)) or _('no help text available')
544 544 except KeyError:
545 545 mod = None
546 546 doc = extensions.disabledext(name)
547 547 if not doc:
548 548 raise error.UnknownCommand(name)
549 549
550 550 if '\n' not in doc:
551 551 head, tail = doc, ""
552 552 else:
553 553 head, tail = doc.split('\n', 1)
554 554 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
555 555 if tail:
556 556 rst.extend(tail.splitlines(True))
557 557 rst.append('\n')
558 558
559 559 if not ui.verbose:
560 560 omitted = _('(some details hidden, use --verbose'
561 561 ' to show complete help)')
562 562 indicateomitted(rst, omitted)
563 563
564 564 if mod:
565 565 try:
566 566 ct = mod.cmdtable
567 567 except AttributeError:
568 568 ct = {}
569 569 modcmds = set([c.partition('|')[0] for c in ct])
570 570 rst.extend(helplist(modcmds.__contains__))
571 571 else:
572 572 rst.append(_("(use 'hg help extensions' for information on enabling"
573 573 " extensions)\n"))
574 574 return rst
575 575
576 576 def helpextcmd(name, subtopic=None):
577 577 cmd, ext, mod = extensions.disabledcmd(ui, name,
578 578 ui.configbool('ui', 'strict'))
579 579 doc = gettext(pycompat.getdoc(mod)).splitlines()[0]
580 580
581 581 rst = listexts(_("'%s' is provided by the following "
582 582 "extension:") % cmd, {ext: doc}, indent=4,
583 583 showdeprecated=True)
584 584 rst.append('\n')
585 585 rst.append(_("(use 'hg help extensions' for information on enabling "
586 586 "extensions)\n"))
587 587 return rst
588 588
589 589
590 590 rst = []
591 591 kw = opts.get('keyword')
592 592 if kw or name is None and any(opts[o] for o in opts):
593 593 matches = topicmatch(ui, commands, name or '')
594 594 helpareas = []
595 595 if opts.get('extension'):
596 596 helpareas += [('extensions', _('Extensions'))]
597 597 if opts.get('command'):
598 598 helpareas += [('commands', _('Commands'))]
599 599 if not helpareas:
600 600 helpareas = [('topics', _('Topics')),
601 601 ('commands', _('Commands')),
602 602 ('extensions', _('Extensions')),
603 603 ('extensioncommands', _('Extension Commands'))]
604 604 for t, title in helpareas:
605 605 if matches[t]:
606 606 rst.append('%s:\n\n' % title)
607 607 rst.extend(minirst.maketable(sorted(matches[t]), 1))
608 608 rst.append('\n')
609 609 if not rst:
610 610 msg = _('no matches')
611 611 hint = _("try 'hg help' for a list of topics")
612 612 raise error.Abort(msg, hint=hint)
613 613 elif name and name != 'shortlist':
614 614 queries = []
615 615 if unknowncmd:
616 616 queries += [helpextcmd]
617 617 if opts.get('extension'):
618 618 queries += [helpext]
619 619 if opts.get('command'):
620 620 queries += [helpcmd]
621 621 if not queries:
622 622 queries = (helptopic, helpcmd, helpext, helpextcmd)
623 623 for f in queries:
624 624 try:
625 625 rst = f(name, subtopic)
626 626 break
627 627 except error.UnknownCommand:
628 628 pass
629 629 else:
630 630 if unknowncmd:
631 631 raise error.UnknownCommand(name)
632 632 else:
633 633 msg = _('no such help topic: %s') % name
634 634 hint = _("try 'hg help --keyword %s'") % name
635 635 raise error.Abort(msg, hint=hint)
636 636 else:
637 637 # program name
638 638 if not ui.quiet:
639 639 rst = [_("Mercurial Distributed SCM\n"), '\n']
640 640 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
641 641
642 642 return ''.join(rst)
643 643
644 644 def formattedhelp(ui, commands, name, keep=None, unknowncmd=False, full=True,
645 645 **opts):
646 646 """get help for a given topic (as a dotted name) as rendered rst
647 647
648 648 Either returns the rendered help text or raises an exception.
649 649 """
650 650 if keep is None:
651 651 keep = []
652 652 else:
653 653 keep = list(keep) # make a copy so we can mutate this later
654 654 fullname = name
655 655 section = None
656 656 subtopic = None
657 657 if name and '.' in name:
658 658 name, remaining = name.split('.', 1)
659 659 remaining = encoding.lower(remaining)
660 660 if '.' in remaining:
661 661 subtopic, section = remaining.split('.', 1)
662 662 else:
663 663 if name in subtopics:
664 664 subtopic = remaining
665 665 else:
666 666 section = remaining
667 667 textwidth = ui.configint('ui', 'textwidth')
668 668 termwidth = ui.termwidth() - 2
669 669 if textwidth <= 0 or termwidth < textwidth:
670 670 textwidth = termwidth
671 671 text = help_(ui, commands, name,
672 672 subtopic=subtopic, unknowncmd=unknowncmd, full=full, **opts)
673 673
674 674 formatted, pruned = minirst.format(text, textwidth, keep=keep,
675 675 section=section)
676 676
677 677 # We could have been given a weird ".foo" section without a name
678 678 # to look for, or we could have simply failed to found "foo.bar"
679 679 # because bar isn't a section of foo
680 680 if section and not (formatted and name):
681 681 raise error.Abort(_("help section not found: %s") % fullname)
682 682
683 683 if 'verbose' in pruned:
684 684 keep.append('omitted')
685 685 else:
686 686 keep.append('notomitted')
687 687 formatted, pruned = minirst.format(text, textwidth, keep=keep,
688 688 section=section)
689 689 return formatted
General Comments 0
You need to be logged in to leave comments. Login now