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