1# -*- coding: utf-8 -*-
2from __future__ import absolute_import, division, print_function
3
4import functools
5import os
6import sys
7from collections import defaultdict
8from textwrap import TextWrapper
9
10from plumbum import colors, local
11from plumbum.cli.i18n import get_translation_for
12from plumbum.lib import getdoc, six
13
14from .switches import (
15    CountOf,
16    Flag,
17    MissingArgument,
18    MissingMandatorySwitch,
19    PositionalArgumentsError,
20    SubcommandError,
21    SwitchCombinationError,
22    SwitchError,
23    UnknownSwitch,
24    WrongArgumentType,
25    switch,
26)
27from .terminal import get_terminal_size
28
29_translation = get_translation_for(__name__)
30T_, ngettext = _translation.gettext, _translation.ngettext
31
32
33class ShowHelp(SwitchError):
34    pass
35
36
37class ShowHelpAll(SwitchError):
38    pass
39
40
41class ShowVersion(SwitchError):
42    pass
43
44
45class SwitchParseInfo(object):
46    __slots__ = ["swname", "val", "index", "__weakref__"]
47
48    def __init__(self, swname, val, index):
49        self.swname = swname
50        self.val = val
51        self.index = index
52
53
54class Subcommand(object):
55    def __init__(self, name, subapplication):
56        self.name = name
57        self.subapplication = subapplication
58
59    def get(self):
60        if isinstance(self.subapplication, str):
61            modname, clsname = self.subapplication.rsplit(".", 1)
62            mod = __import__(modname, None, None, "*")
63            try:
64                cls = getattr(mod, clsname)
65            except AttributeError:
66                raise ImportError("cannot import name {}".format(clsname))
67            self.subapplication = cls
68        return self.subapplication
69
70    def __repr__(self):
71        return T_("Subcommand({self.name}, {self.subapplication})").format(self=self)
72
73
74_switch_groups = ["Switches", "Meta-switches"]
75_switch_groups_l10n = [T_("Switches"), T_("Meta-switches")]
76
77
78# ===================================================================================================
79# CLI Application base class
80# ===================================================================================================
81
82
83class Application(object):
84    """The base class for CLI applications; your "entry point" class should derive from it,
85    define the relevant switch functions and attributes, and the ``main()`` function.
86    The class defines two overridable "meta switches" for version (``-v``, ``--version``)
87    help (``-h``, ``--help``), and help-all (``--help-all``).
88
89    The signature of the main function matters: any positional arguments (e.g., non-switch
90    arguments) given on the command line are passed to the ``main()`` function; if you wish
91    to allow unlimited number of positional arguments, use varargs (``*args``). The names
92    of the arguments will be shown in the help message.
93
94    The classmethod ``run`` serves as the entry point of the class. It parses the command-line
95    arguments, invokes switch functions and enters ``main``. You should **not override** this
96    method.
97
98    Usage::
99
100        class FileCopier(Application):
101            stat = Flag("p", "copy stat info as well")
102
103            def main(self, src, dst):
104                if self.stat:
105                    shutil.copy2(src, dst)
106                else:
107                    shutil.copy(src, dst)
108
109        if __name__ == "__main__":
110            FileCopier.run()
111
112    There are several class-level attributes you may set:
113
114    * ``PROGNAME`` - the name of the program; if ``None`` (the default), it is set to the
115      name of the executable (``argv[0]``); can be in color. If only a color, will be applied to the name.
116
117    * ``VERSION`` - the program's version (defaults to ``1.0``, can be in color)
118
119    * ``DESCRIPTION`` - a short description of your program (shown in help). If not set,
120      the class' ``__doc__`` will be used. Can be in color.
121
122    * ``DESCRIPTION_MORE`` - a detailed description of your program (shown in help). The text will be printed
123      by paragraphs (specified by empty lines between them). The indentation of each paragraph will be the
124      indentation of its first line. List items are identified by their first non-whitespace character being
125      one of '-', '*', and '/'; so that they are not combined with preceding paragraphs. Bullet '/' is
126      "invisible", meaning that the bullet itself will not be printed to the output.
127
128    * ``USAGE`` - the usage line (shown in help).
129
130    * ``COLOR_USAGE_TITLE`` - The color of the usage line's header.
131
132    * ``COLOR_USAGE`` - The color of the usage line.
133
134    * ``COLOR_GROUPS`` - A dictionary that sets colors for the groups, like Meta-switches, Switches,
135      and Subcommands.
136
137    * ``COLOR_GROUP_TITLES`` - A dictionary that sets colors for the group titles. If the dictionary is empty,
138      it defaults to ``COLOR_GROUPS``.
139
140    * ``SUBCOMMAND_HELPMSG`` - Controls the printing of extra "see subcommand -h" help message.
141      Default is a message, set to False to remove.
142
143    * ``ALLOW_ABBREV`` - Controls whether partial switch names are supported, for example '--ver' will match
144      '--verbose'. Default is False for backward consistency with previous plumbum releases. Note that ambiguous
145      abbreviations will not match, for example if --foothis and --foothat are defined, then --foo will not match.
146
147    A note on sub-commands: when an application is the root, its ``parent`` attribute is set to
148    ``None``. When it is used as a nested-command, ``parent`` will point to its direct ancestor.
149    Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute
150    will hold the chosen sub-application and its command-line arguments (a tuple); otherwise, it
151    will be set to ``None``
152
153    """
154
155    PROGNAME = None
156    DESCRIPTION = None
157    DESCRIPTION_MORE = None
158    VERSION = None
159    USAGE = None
160    COLOR_USAGE = None
161    COLOR_USAGE_TITLE = None
162    COLOR_GROUPS = None
163    COLOR_GROUP_TITLES = None
164    CALL_MAIN_IF_NESTED_COMMAND = True
165    SUBCOMMAND_HELPMSG = T_("see '{parent} {sub} --help' for more info")
166    ALLOW_ABBREV = False
167
168    parent = None
169    nested_command = None
170    _unbound_switches = ()
171
172    def __new__(cls, executable=None):
173        """Allows running the class directly as a shortcut for main.
174        This is necessary for some setup scripts that want a single function,
175        instead of an expression with a dot in it."""
176
177        if executable is None:
178            return cls.run()
179            # This return value was not a class instance, so __init__ is never called
180        else:
181            return super(Application, cls).__new__(cls)
182
183    def __init__(self, executable):
184        # Filter colors
185
186        if self.PROGNAME is None:
187            self.PROGNAME = os.path.basename(executable)
188        elif isinstance(self.PROGNAME, colors._style):
189            self.PROGNAME = self.PROGNAME | os.path.basename(executable)
190        elif colors.filter(self.PROGNAME) == "":
191            self.PROGNAME = colors.extract(self.PROGNAME) | os.path.basename(executable)
192        if self.DESCRIPTION is None:
193            self.DESCRIPTION = getdoc(self)
194
195        # Allow None for the colors
196        self.COLOR_GROUPS = defaultdict(
197            lambda: colors.do_nothing,
198            dict() if type(self).COLOR_GROUPS is None else type(self).COLOR_GROUPS,
199        )
200
201        self.COLOR_GROUP_TITLES = defaultdict(
202            lambda: colors.do_nothing,
203            self.COLOR_GROUPS
204            if type(self).COLOR_GROUP_TITLES is None
205            else type(self).COLOR_GROUP_TITLES,
206        )
207        if type(self).COLOR_USAGE is None:
208            self.COLOR_USAGE = colors.do_nothing
209
210        self.executable = executable
211        self._switches_by_name = {}
212        self._switches_by_func = {}
213        self._switches_by_envar = {}
214        self._subcommands = {}
215
216        for cls in reversed(type(self).mro()):
217            for obj in cls.__dict__.values():
218                if isinstance(obj, Subcommand):
219                    name = colors.filter(obj.name)
220                    if name.startswith("-"):
221                        raise SubcommandError(
222                            T_("Sub-command names cannot start with '-'")
223                        )
224                    # it's okay for child classes to override sub-commands set by their parents
225                    self._subcommands[name] = obj
226                    continue
227
228                swinfo = getattr(obj, "_switch_info", None)
229                if not swinfo:
230                    continue
231                for name in swinfo.names:
232                    if name in self._unbound_switches:
233                        continue
234                    if (
235                        name in self._switches_by_name
236                        and not self._switches_by_name[name].overridable
237                    ):
238                        raise SwitchError(
239                            T_(
240                                "Switch {name} already defined and is not overridable"
241                            ).format(name=name)
242                        )
243                    self._switches_by_name[name] = swinfo
244                    self._switches_by_func[swinfo.func] = swinfo
245                    if swinfo.envname:
246                        self._switches_by_envar[swinfo.envname] = swinfo
247
248    @property
249    def root_app(self):
250        return self.parent.root_app if self.parent else self
251
252    @classmethod
253    def unbind_switches(cls, *switch_names):
254        """Unbinds the given switch names from this application. For example
255
256        ::
257
258            class MyApp(cli.Application):
259                pass
260            MyApp.unbind_switches("--version")
261
262        """
263        cls._unbound_switches += tuple(
264            name.lstrip("-") for name in switch_names if name
265        )
266
267    @classmethod
268    def subcommand(cls, name, subapp=None):
269        """Registers the given sub-application as a sub-command of this one. This method can be
270        used both as a decorator and as a normal ``classmethod``::
271
272            @MyApp.subcommand("foo")
273            class FooApp(cli.Application):
274                pass
275
276        Or ::
277
278            MyApp.subcommand("foo", FooApp)
279
280        .. versionadded:: 1.1
281
282        .. versionadded:: 1.3
283            The sub-command can also be a string, in which case it is treated as a
284            fully-qualified class name and is imported on demand. For example,
285
286            MyApp.subcommand("foo", "fully.qualified.package.FooApp")
287
288        """
289
290        def wrapper(subapp):
291            attrname = "_subcommand_{}".format(
292                subapp if isinstance(subapp, str) else subapp.__name__
293            )
294            setattr(cls, attrname, Subcommand(name, subapp))
295            return subapp
296
297        return wrapper(subapp) if subapp else wrapper
298
299    def _get_partial_matches(self, partialname):
300        matches = []
301        for switch in self._switches_by_name:
302            if switch.startswith(partialname):
303                matches += [
304                    switch,
305                ]
306        return matches
307
308    def _parse_args(self, argv):
309        tailargs = []
310        swfuncs = {}
311        index = 0
312
313        while argv:
314            index += 1
315            a = argv.pop(0)
316            val = None
317            if a == "--":
318                # end of options, treat the rest as tailargs
319                tailargs.extend(argv)
320                break
321
322            if a in self._subcommands:
323                subcmd = self._subcommands[a].get()
324                self.nested_command = (
325                    subcmd,
326                    [self.PROGNAME + " " + self._subcommands[a].name] + argv,
327                )
328                break
329
330            elif a.startswith("--") and len(a) >= 3:
331                # [--name], [--name=XXX], [--name, XXX], [--name, ==, XXX],
332                # [--name=, XXX], [--name, =XXX]
333                eqsign = a.find("=")
334                if eqsign >= 0:
335                    name = a[2:eqsign]
336                    argv.insert(0, a[eqsign:])
337                else:
338                    name = a[2:]
339
340                if self.ALLOW_ABBREV:
341                    partials = self._get_partial_matches(name)
342                    if len(partials) == 1:
343                        name = partials[0]
344                    elif len(partials) > 1:
345                        raise UnknownSwitch(
346                            T_("Ambiguous partial switch {0}").format("--" + name)
347                        )
348
349                swname = "--" + name
350                if name not in self._switches_by_name:
351                    raise UnknownSwitch(T_("Unknown switch {0}").format(swname))
352                swinfo = self._switches_by_name[name]
353                if swinfo.argtype:
354                    if not argv:
355                        raise MissingArgument(
356                            T_("Switch {0} requires an argument").format(swname)
357                        )
358                    a = argv.pop(0)
359                    if a and a[0] == "=":
360                        if len(a) >= 2:
361                            val = a[1:]
362                        else:
363                            if not argv:
364                                raise MissingArgument(
365                                    T_("Switch {0} requires an argument").format(swname)
366                                )
367                            val = argv.pop(0)
368                    else:
369                        val = a
370
371            elif a.startswith("-") and len(a) >= 2:
372                # [-a], [-a, XXX], [-aXXX], [-abc]
373                name = a[1]
374                swname = "-" + name
375                if name not in self._switches_by_name:
376                    raise UnknownSwitch(T_("Unknown switch {0}").format(swname))
377                swinfo = self._switches_by_name[name]
378                if swinfo.argtype:
379                    if len(a) >= 3:
380                        val = a[2:]
381                    else:
382                        if not argv:
383                            raise MissingArgument(
384                                T_("Switch {0} requires an argument").format(swname)
385                            )
386                        val = argv.pop(0)
387                elif len(a) >= 3:
388                    argv.insert(0, "-" + a[2:])
389
390            else:
391                if a.startswith("-"):
392                    raise UnknownSwitch(T_("Unknown switch {0}").format(a))
393                tailargs.append(a)
394                continue
395
396            # handle argument
397            val = self._handle_argument(val, swinfo.argtype, name)
398
399            if swinfo.func in swfuncs:
400                if swinfo.list:
401                    swfuncs[swinfo.func].val[0].append(val)
402                else:
403                    if swfuncs[swinfo.func].swname == swname:
404                        raise SwitchError(T_("Switch {0} already given").format(swname))
405                    else:
406                        raise SwitchError(
407                            T_("Switch {0} already given ({1} is equivalent)").format(
408                                swfuncs[swinfo.func].swname, swname
409                            )
410                        )
411            else:
412                if swinfo.list:
413                    swfuncs[swinfo.func] = SwitchParseInfo(swname, ([val],), index)
414                elif val is NotImplemented:
415                    swfuncs[swinfo.func] = SwitchParseInfo(swname, (), index)
416                else:
417                    swfuncs[swinfo.func] = SwitchParseInfo(swname, (val,), index)
418
419        # Extracting arguments from environment variables
420        envindex = 0
421        for env, swinfo in self._switches_by_envar.items():
422            envindex -= 1
423            envval = local.env.get(env)
424            if envval is None:
425                continue
426
427            if swinfo.func in swfuncs:
428                continue  # skip if overridden by command line arguments
429
430            val = self._handle_argument(envval, swinfo.argtype, env)
431            envname = "${}".format(env)
432            if swinfo.list:
433                # multiple values over environment variables are not supported,
434                # this will require some sort of escaping and separator convention
435                swfuncs[swinfo.func] = SwitchParseInfo(envname, ([val],), envindex)
436            elif val is NotImplemented:
437                swfuncs[swinfo.func] = SwitchParseInfo(envname, (), envindex)
438            else:
439                swfuncs[swinfo.func] = SwitchParseInfo(envname, (val,), envindex)
440
441        return swfuncs, tailargs
442
443    @classmethod
444    def autocomplete(cls, argv):
445        """This is supplied to make subclassing and testing argument completion methods easier"""
446        pass
447
448    @staticmethod
449    def _handle_argument(val, argtype, name):
450        if argtype:
451            try:
452                return argtype(val)
453            except (TypeError, ValueError):
454                ex = sys.exc_info()[1]  # compat
455                raise WrongArgumentType(
456                    T_(
457                        "Argument of {name} expected to be {argtype}, not {val!r}:\n    {ex!r}"
458                    ).format(name=name, argtype=argtype, val=val, ex=ex)
459                )
460        else:
461            return NotImplemented
462
463    def _validate_args(self, swfuncs, tailargs):
464        if six.get_method_function(self.help) in swfuncs:
465            raise ShowHelp()
466        if six.get_method_function(self.helpall) in swfuncs:
467            raise ShowHelpAll()
468        if six.get_method_function(self.version) in swfuncs:
469            raise ShowVersion()
470
471        requirements = {}
472        exclusions = {}
473        for swinfo in self._switches_by_func.values():
474            if swinfo.mandatory and not swinfo.func in swfuncs:
475                raise MissingMandatorySwitch(
476                    T_("Switch {0} is mandatory").format(
477                        "/".join(
478                            ("-" if len(n) == 1 else "--") + n for n in swinfo.names
479                        )
480                    )
481                )
482            requirements[swinfo.func] = {
483                self._switches_by_name[req] for req in swinfo.requires
484            }
485            exclusions[swinfo.func] = {
486                self._switches_by_name[exc] for exc in swinfo.excludes
487            }
488
489        # TODO: compute topological order
490
491        gotten = set(swfuncs.keys())
492        for func in gotten:
493            missing = {f.func for f in requirements[func]} - gotten
494            if missing:
495                raise SwitchCombinationError(
496                    T_("Given {0}, the following are missing {1}").format(
497                        swfuncs[func].swname,
498                        [self._switches_by_func[f].names[0] for f in missing],
499                    )
500                )
501            invalid = {f.func for f in exclusions[func]} & gotten
502            if invalid:
503                raise SwitchCombinationError(
504                    T_("Given {0}, the following are invalid {1}").format(
505                        swfuncs[func].swname, [swfuncs[f].swname for f in invalid]
506                    )
507                )
508
509        m = six.getfullargspec(self.main)
510        max_args = six.MAXSIZE if m.varargs else len(m.args) - 1
511        min_args = len(m.args) - 1 - (len(m.defaults) if m.defaults else 0)
512        if len(tailargs) < min_args:
513            raise PositionalArgumentsError(
514                ngettext(
515                    "Expected at least {0} positional argument, got {1}",
516                    "Expected at least {0} positional arguments, got {1}",
517                    min_args,
518                ).format(min_args, tailargs)
519            )
520        elif len(tailargs) > max_args:
521            raise PositionalArgumentsError(
522                ngettext(
523                    "Expected at most {0} positional argument, got {1}",
524                    "Expected at most {0} positional arguments, got {1}",
525                    max_args,
526                ).format(max_args, tailargs)
527            )
528
529        # Positional arguement validataion
530        if hasattr(self.main, "positional"):
531            tailargs = self._positional_validate(
532                tailargs,
533                self.main.positional,
534                self.main.positional_varargs,
535                m.args[1:],
536                m.varargs,
537            )
538
539        elif hasattr(m, "annotations"):
540            args_names = list(m.args[1:])
541            positional = [None] * len(args_names)
542            varargs = None
543
544            # All args are positional, so convert kargs to positional
545            for item in m.annotations:
546                if item == m.varargs:
547                    varargs = m.annotations[item]
548                elif item != "return":
549                    positional[args_names.index(item)] = m.annotations[item]
550
551            tailargs = self._positional_validate(
552                tailargs, positional, varargs, m.args[1:], m.varargs
553            )
554
555        ordered = [
556            (f, a)
557            for _, f, a in sorted((sf.index, f, sf.val) for f, sf in swfuncs.items())
558        ]
559        return ordered, tailargs
560
561    def _positional_validate(self, args, validator_list, varargs, argnames, varargname):
562        """Makes sure args follows the validation given input"""
563        out_args = list(args)
564
565        for i in range(min(len(args), len(validator_list))):
566
567            if validator_list[i] is not None:
568                out_args[i] = self._handle_argument(
569                    args[i], validator_list[i], argnames[i]
570                )
571
572        if len(args) > len(validator_list):
573            if varargs is not None:
574                out_args[len(validator_list) :] = [
575                    self._handle_argument(a, varargs, varargname)
576                    for a in args[len(validator_list) :]
577                ]
578            else:
579                out_args[len(validator_list) :] = args[len(validator_list) :]
580
581        return out_args
582
583    @classmethod
584    def run(cls, argv=None, exit=True):  # @ReservedAssignment
585        """
586        Runs the application, taking the arguments from ``sys.argv`` by default if
587        nothing is passed. If ``exit`` is
588        ``True`` (the default), the function will exit with the appropriate return code;
589        otherwise it will return a tuple of ``(inst, retcode)``, where ``inst`` is the
590        application instance created internally by this function and ``retcode`` is the
591        exit code of the application.
592
593        .. note::
594           Setting ``exit`` to ``False`` is intendend for testing/debugging purposes only -- do
595           not override it in other situations.
596        """
597        if argv is None:
598            argv = sys.argv
599        cls.autocomplete(argv)
600        argv = list(argv)
601        inst = cls(argv.pop(0))
602        retcode = 0
603        try:
604            swfuncs, tailargs = inst._parse_args(argv)
605            ordered, tailargs = inst._validate_args(swfuncs, tailargs)
606        except ShowHelp:
607            inst.help()
608        except ShowHelpAll:
609            inst.helpall()
610        except ShowVersion:
611            inst.version()
612        except SwitchError:
613            ex = sys.exc_info()[1]  # compatibility with python 2.5
614            print(T_("Error: {0}").format(ex))
615            print(T_("------"))
616            inst.help()
617            retcode = 2
618        else:
619            for f, a in ordered:
620                f(inst, *a)
621
622            cleanup = None
623            if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND:
624                retcode = inst.main(*tailargs)
625                cleanup = functools.partial(inst.cleanup, retcode)
626            if not retcode and inst.nested_command:
627                subapp, argv = inst.nested_command
628                subapp.parent = inst
629                inst, retcode = subapp.run(argv, exit=False)
630
631            if cleanup:
632                cleanup()
633
634            if retcode is None:
635                retcode = 0
636
637        if exit:
638            sys.exit(retcode)
639        else:
640            return inst, retcode
641
642    @classmethod
643    def invoke(cls, *args, **switches):
644        """Invoke this application programmatically (as a function), in the same way ``run()``
645        would. There are two key differences: the return value of ``main()`` is not converted to
646        an integer (returned as-is), and exceptions are not swallowed either.
647
648        :param args: any positional arguments for ``main()``
649        :param switches: command-line switches are passed as keyword arguments,
650                         e.g., ``foo=5`` for ``--foo=5``
651        """
652
653        inst = cls("")
654
655        swfuncs = inst._parse_kwd_args(switches)
656        ordered, tailargs = inst._validate_args(swfuncs, args)
657        for f, a in ordered:
658            f(inst, *a)
659
660        cleanup = None
661        if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND:
662            retcode = inst.main(*tailargs)
663            cleanup = functools.partial(inst.cleanup, retcode)
664        if not retcode and inst.nested_command:
665            subapp, argv = inst.nested_command
666            subapp.parent = inst
667            inst, retcode = subapp.run(argv, exit=False)
668
669        if cleanup:
670            cleanup()
671
672        return inst, retcode
673
674    def _parse_kwd_args(self, switches):
675        """Parses keywords (positional arguments), used by invoke."""
676        swfuncs = {}
677        for index, (swname, val) in enumerate(switches.items(), 1):
678            switch = getattr(type(self), swname)
679            swinfo = self._switches_by_func[switch._switch_info.func]
680            if isinstance(switch, CountOf):
681                p = (range(val),)
682            elif swinfo.list and not hasattr(val, "__iter__"):
683                raise SwitchError(
684                    T_("Switch {0} must be a sequence (iterable)").format(swname)
685                )
686            elif not swinfo.argtype:
687                # a flag
688                if val not in (True, False, None, Flag):
689                    raise SwitchError(T_("Switch {0} is a boolean flag").format(swname))
690                p = ()
691            else:
692                p = (val,)
693            swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index)
694        return swfuncs
695
696    def main(self, *args):
697        """Implement me (no need to call super)"""
698        if self._subcommands:
699            if args:
700                print(T_("Unknown sub-command '{0}'").format(args[0]))
701                print(T_("------"))
702                self.help()
703                return 1
704            if not self.nested_command:
705                print(T_("No sub-command given"))
706                print(T_("------"))
707                self.help()
708                return 1
709        else:
710            print(T_("main() not implemented"))
711            return 1
712
713    def cleanup(self, retcode):
714        """Called after ``main()`` and all sub-applications have executed, to perform any necessary cleanup.
715
716        :param retcode: the return code of ``main()``
717        """
718
719    @switch(
720        ["--help-all"],
721        overridable=True,
722        group="Meta-switches",
723        help=T_("""Prints help messages of all sub-commands and quits"""),
724    )
725    def helpall(self):
726        """Prints help messages of all sub-commands and quits"""
727        self.help()
728        print("")
729
730        if self._subcommands:
731            for name, subcls in sorted(self._subcommands.items()):
732                subapp = (subcls.get())("{} {}".format(self.PROGNAME, name))
733                subapp.parent = self
734                for si in subapp._switches_by_func.values():
735                    if si.group == "Meta-switches":
736                        si.group = "Hidden-switches"
737                subapp.helpall()
738
739    @switch(
740        ["-h", "--help"],
741        overridable=True,
742        group="Meta-switches",
743        help=T_("""Prints this help message and quits"""),
744    )
745    def help(self):  # @ReservedAssignment
746        """Prints this help message and quits"""
747        if self._get_prog_version():
748            self.version()
749            print("")
750        if self.DESCRIPTION:
751            print(self.DESCRIPTION.strip() + "\n")
752
753        def split_indentation(s):
754            """Identifies the initial indentation (all spaces) of the string and returns the indentation as well
755            as the remainder of the line.
756            """
757            i = 0
758            while i < len(s) and s[i] == " ":
759                i += 1
760            return s[:i], s[i:]
761
762        def paragraphs(text):
763            """Yields each paragraph of text along with its initial and subsequent indentations to be used by
764            textwrap.TextWrapper.
765
766            Identifies list items from their first non-space character being one of bullets '-', '*', and '/'.
767            However, bullet '/' is invisible and is removed from the list item.
768
769            :param text: The text to separate into paragraphs
770            """
771
772            paragraph = None
773            initial_indent = ""
774            subsequent_indent = ""
775
776            def current():
777                """Yields the current result if present."""
778                if paragraph:
779                    yield paragraph, initial_indent, subsequent_indent
780
781            for part in text.lstrip("\n").split("\n"):
782                indent, line = split_indentation(part)
783
784                if len(line) == 0:
785                    # Starting a new paragraph
786                    for item in current():
787                        yield item
788                    yield "", "", ""
789
790                    paragraph = None
791                    initial_indent = ""
792                    subsequent_indent = ""
793                else:
794                    # Adding to current paragraph
795                    def is_list_item(line):
796                        """Returns true if the first element of 'line' is a bullet character."""
797                        bullets = ["-", "*", "/"]
798                        return line[0] in bullets
799
800                    def has_invisible_bullet(line):
801                        """Returns true if the first element of 'line' is the invisible bullet ('/')."""
802                        return line[0] == "/"
803
804                    if is_list_item(line):
805                        # Done with current paragraph
806                        for item in current():
807                            yield item
808
809                        if has_invisible_bullet(line):
810                            line = line[1:]
811
812                        paragraph = line
813                        initial_indent = indent
814
815                        # Calculate extra indentation for subsequent lines of this list item
816                        i = 1
817                        while i < len(line) and line[i] == " ":
818                            i += 1
819                        subsequent_indent = indent + " " * i
820                    else:
821                        if not paragraph:
822                            # Start a new paragraph
823                            paragraph = line
824                            initial_indent = indent
825                            subsequent_indent = indent
826                        else:
827                            # Add to current paragraph
828                            paragraph = paragraph + " " + line
829
830            for item in current():
831                yield item
832
833        def wrapped_paragraphs(text, width):
834            """Yields each line of each paragraph of text after wrapping them on 'width' number of columns.
835
836            :param text: The text to yield wrapped lines of
837            :param width: The width of the wrapped output
838            """
839            if not text:
840                return
841
842            width = max(width, 1)
843
844            for paragraph, initial_indent, subsequent_indent in paragraphs(text):
845                wrapper = TextWrapper(
846                    width,
847                    initial_indent=initial_indent,
848                    subsequent_indent=subsequent_indent,
849                )
850                w = wrapper.wrap(paragraph)
851                for line in w:
852                    yield line
853                if len(w) == 0:
854                    yield ""
855
856        cols, _ = get_terminal_size()
857        for line in wrapped_paragraphs(self.DESCRIPTION_MORE, cols):
858            print(line)
859
860        m = six.getfullargspec(self.main)
861        tailargs = m.args[1:]  # skip self
862        if m.defaults:
863            for i, d in enumerate(reversed(m.defaults)):
864                tailargs[-i - 1] = "[{}={}]".format(tailargs[-i - 1], d)
865        if m.varargs:
866            tailargs.append(
867                "{}...".format(
868                    m.varargs,
869                )
870            )
871        tailargs = " ".join(tailargs)
872
873        utc = self.COLOR_USAGE_TITLE if self.COLOR_USAGE_TITLE else self.COLOR_USAGE
874        print(utc | T_("Usage:"))
875
876        with self.COLOR_USAGE:
877            if not self.USAGE:
878                if self._subcommands:
879                    self.USAGE = T_(
880                        "    {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n"
881                    )
882                else:
883                    self.USAGE = T_("    {progname} [SWITCHES] {tailargs}\n")
884            print(
885                self.USAGE.format(
886                    progname=colors.filter(self.PROGNAME), tailargs=tailargs
887                )
888            )
889
890        by_groups = {}
891        for si in self._switches_by_func.values():
892            if si.group not in by_groups:
893                by_groups[si.group] = []
894            by_groups[si.group].append(si)
895
896        def switchs(by_groups, show_groups):
897            for grp, swinfos in sorted(by_groups.items(), key=lambda item: item[0]):
898                if show_groups:
899                    lgrp = T_(grp) if grp in _switch_groups else grp
900                    print(self.COLOR_GROUP_TITLES[grp] | lgrp + ":")
901
902                for si in sorted(swinfos, key=lambda si: si.names):
903                    swnames = ", ".join(
904                        ("-" if len(n) == 1 else "--") + n
905                        for n in si.names
906                        if n in self._switches_by_name
907                        and self._switches_by_name[n] == si
908                    )
909                    if si.argtype:
910                        if hasattr(si.argtype, "__name__"):
911                            typename = si.argtype.__name__
912                        else:
913                            typename = str(si.argtype)
914                        argtype = " {}:{}".format(si.argname.upper(), typename)
915                    else:
916                        argtype = ""
917                    prefix = swnames + argtype
918                    yield si, prefix, self.COLOR_GROUPS[grp]
919
920                if show_groups:
921                    print("")
922
923        sw_width = (
924            max(len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4
925        )
926        description_indent = "    {0}{1}{2}"
927        wrapper = TextWrapper(width=max(cols - min(sw_width, 60), 50) - 6)
928        indentation = "\n" + " " * (cols - wrapper.width)
929
930        for switch_info, prefix, color in switchs(by_groups, True):
931            help = switch_info.help  # @ReservedAssignment
932            if switch_info.list:
933                help += T_("; may be given multiple times")
934            if switch_info.mandatory:
935                help += T_("; required")
936            if switch_info.requires:
937                help += T_("; requires {0}").format(
938                    ", ".join(
939                        (("-" if len(switch) == 1 else "--") + switch)
940                        for switch in switch_info.requires
941                    )
942                )
943            if switch_info.excludes:
944                help += T_("; excludes {0}").format(
945                    ", ".join(
946                        (("-" if len(switch) == 1 else "--") + switch)
947                        for switch in switch_info.excludes
948                    )
949                )
950
951            msg = indentation.join(
952                wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))
953            )
954
955            if len(prefix) + wrapper.width >= cols:
956                padding = indentation
957            else:
958                padding = " " * max(cols - wrapper.width - len(prefix) - 4, 1)
959            print(description_indent.format(color | prefix, padding, color | msg))
960
961        if self._subcommands:
962            gc = self.COLOR_GROUP_TITLES["Sub-commands"]
963            print(gc | T_("Sub-commands:"))
964            for name, subcls in sorted(self._subcommands.items()):
965                with gc:
966                    subapp = subcls.get()
967                    doc = subapp.DESCRIPTION if subapp.DESCRIPTION else getdoc(subapp)
968                    if self.SUBCOMMAND_HELPMSG:
969                        help = doc + "; " if doc else ""  # @ReservedAssignment
970                        help += self.SUBCOMMAND_HELPMSG.format(
971                            parent=self.PROGNAME, sub=name
972                        )
973                    else:
974                        help = doc if doc else ""  # @ReservedAssignment
975
976                    msg = indentation.join(
977                        wrapper.wrap(" ".join(l.strip() for l in help.splitlines()))
978                    )
979
980                    if len(name) + wrapper.width >= cols:
981                        padding = indentation
982                    else:
983                        padding = " " * max(cols - wrapper.width - len(name) - 4, 1)
984                    if colors.contains_colors(subcls.name):
985                        bodycolor = colors.extract(subcls.name)
986                    else:
987                        bodycolor = gc
988
989                    print(
990                        description_indent.format(
991                            subcls.name, padding, bodycolor | colors.filter(msg)
992                        )
993                    )
994
995    def _get_prog_version(self):
996        ver = None
997        curr = self
998        while curr is not None:
999            ver = getattr(curr, "VERSION", None)
1000            if ver is not None:
1001                return ver
1002            curr = curr.parent
1003        return ver
1004
1005    @switch(
1006        ["-v", "--version"],
1007        overridable=True,
1008        group="Meta-switches",
1009        help=T_("""Prints the program's version and quits"""),
1010    )
1011    def version(self):
1012        """Prints the program's version and quits"""
1013        ver = self._get_prog_version()
1014        ver_name = ver if ver is not None else T_("(version not set)")
1015        print("{} {}".format(self.PROGNAME, ver_name))
1016