1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import sys
8import argparse
9from collections import defaultdict, OrderedDict
10
11class ArgumentUsageError(Exception):
12    """Exception class you can raise (and catch) in order to show the help"""
13    def __init__(self, message, subcommand=None):
14        self.message = message
15        self.subcommand = subcommand
16
17class ArgumentParser(argparse.ArgumentParser):
18    """Our own version of argparse's ArgumentParser"""
19    def __init__(self, *args, **kwargs):
20        kwargs.setdefault('formatter_class', OeHelpFormatter)
21        self._subparser_groups = OrderedDict()
22        super(ArgumentParser, self).__init__(*args, **kwargs)
23        self._positionals.title = 'arguments'
24        self._optionals.title = 'options'
25
26    def error(self, message):
27        """error(message: string)
28
29        Prints a help message incorporating the message to stderr and
30        exits.
31        """
32        self._print_message('%s: error: %s\n' % (self.prog, message), sys.stderr)
33        self.print_help(sys.stderr)
34        sys.exit(2)
35
36    def error_subcommand(self, message, subcommand):
37        if subcommand:
38            action = self._get_subparser_action()
39            try:
40                subparser = action._name_parser_map[subcommand]
41            except KeyError:
42                self.error('no subparser for name "%s"' % subcommand)
43            else:
44                subparser.error(message)
45
46        self.error(message)
47
48    def add_subparsers(self, *args, **kwargs):
49        if 'dest' not in kwargs:
50            kwargs['dest'] = '_subparser_name'
51
52        ret = super(ArgumentParser, self).add_subparsers(*args, **kwargs)
53        # Need a way of accessing the parent parser
54        ret._parent_parser = self
55        # Ensure our class gets instantiated
56        ret._parser_class = ArgumentSubParser
57        # Hacky way of adding a method to the subparsers object
58        ret.add_subparser_group = self.add_subparser_group
59        return ret
60
61    def add_subparser_group(self, groupname, groupdesc, order=0):
62        self._subparser_groups[groupname] = (groupdesc, order)
63
64    def parse_args(self, args=None, namespace=None):
65        """Parse arguments, using the correct subparser to show the error."""
66        args, argv = self.parse_known_args(args, namespace)
67        if argv:
68            message = 'unrecognized arguments: %s' % ' '.join(argv)
69            if self._subparsers:
70                subparser = self._get_subparser(args)
71                subparser.error(message)
72            else:
73                self.error(message)
74            sys.exit(2)
75        return args
76
77    def _get_subparser(self, args):
78        action = self._get_subparser_action()
79        if action.dest == argparse.SUPPRESS:
80            self.error('cannot get subparser, the subparser action dest is suppressed')
81
82        name = getattr(args, action.dest)
83        try:
84            return action._name_parser_map[name]
85        except KeyError:
86            self.error('no subparser for name "%s"' % name)
87
88    def _get_subparser_action(self):
89        if not self._subparsers:
90            self.error('cannot return the subparser action, no subparsers added')
91
92        for action in self._subparsers._group_actions:
93            if isinstance(action, argparse._SubParsersAction):
94                return action
95
96
97class ArgumentSubParser(ArgumentParser):
98    def __init__(self, *args, **kwargs):
99        if 'group' in kwargs:
100            self._group = kwargs.pop('group')
101        if 'order' in kwargs:
102            self._order = kwargs.pop('order')
103        super(ArgumentSubParser, self).__init__(*args, **kwargs)
104
105    def parse_known_args(self, args=None, namespace=None):
106        # This works around argparse not handling optional positional arguments being
107        # intermixed with other options. A pretty horrible hack, but we're not left
108        # with much choice given that the bug in argparse exists and it's difficult
109        # to subclass.
110        # Borrowed from http://stackoverflow.com/questions/20165843/argparse-how-to-handle-variable-number-of-arguments-nargs
111        # with an extra workaround (in format_help() below) for the positional
112        # arguments disappearing from the --help output, as well as structural tweaks.
113        # Originally simplified from http://bugs.python.org/file30204/test_intermixed.py
114        positionals = self._get_positional_actions()
115        for action in positionals:
116            # deactivate positionals
117            action.save_nargs = action.nargs
118            action.nargs = 0
119
120        namespace, remaining_args = super(ArgumentSubParser, self).parse_known_args(args, namespace)
121        for action in positionals:
122            # remove the empty positional values from namespace
123            if hasattr(namespace, action.dest):
124                delattr(namespace, action.dest)
125        for action in positionals:
126            action.nargs = action.save_nargs
127        # parse positionals
128        namespace, extras = super(ArgumentSubParser, self).parse_known_args(remaining_args, namespace)
129        return namespace, extras
130
131    def format_help(self):
132        # Quick, restore the positionals!
133        positionals = self._get_positional_actions()
134        for action in positionals:
135            if hasattr(action, 'save_nargs'):
136                action.nargs = action.save_nargs
137        return super(ArgumentParser, self).format_help()
138
139
140class OeHelpFormatter(argparse.HelpFormatter):
141    def _format_action(self, action):
142        if hasattr(action, '_get_subactions'):
143            # subcommands list
144            groupmap = defaultdict(list)
145            ordermap = {}
146            subparser_groups = action._parent_parser._subparser_groups
147            groups = sorted(subparser_groups.keys(), key=lambda item: subparser_groups[item][1], reverse=True)
148            for subaction in self._iter_indented_subactions(action):
149                parser = action._name_parser_map[subaction.dest]
150                group = getattr(parser, '_group', None)
151                groupmap[group].append(subaction)
152                if group not in groups:
153                    groups.append(group)
154                order = getattr(parser, '_order', 0)
155                ordermap[subaction.dest] = order
156
157            lines = []
158            if len(groupmap) > 1:
159                groupindent = '  '
160            else:
161                groupindent = ''
162            for group in groups:
163                subactions = groupmap[group]
164                if not subactions:
165                    continue
166                if groupindent:
167                    if not group:
168                        group = 'other'
169                    groupdesc = subparser_groups.get(group, (group, 0))[0]
170                    lines.append('  %s:' % groupdesc)
171                for subaction in sorted(subactions, key=lambda item: ordermap[item.dest], reverse=True):
172                    lines.append('%s%s' % (groupindent, self._format_action(subaction).rstrip()))
173            return '\n'.join(lines)
174        else:
175            return super(OeHelpFormatter, self)._format_action(action)
176
177def int_positive(value):
178    ivalue = int(value)
179    if ivalue <= 0:
180        raise argparse.ArgumentTypeError(
181                "%s is not a positive int value" % value)
182    return ivalue
183