1import importlib
2import os
3import pkgutil
4import signal
5import sys
6from collections import defaultdict
7from difflib import get_close_matches
8from inspect import getmembers
9
10from colorama import Style
11
12from conans import __version__ as client_version
13from conans.cli.command import ConanSubCommand
14from conans.cli.exit_codes import SUCCESS, ERROR_MIGRATION, ERROR_GENERAL, USER_CTRL_C, \
15    ERROR_SIGTERM, USER_CTRL_BREAK, ERROR_INVALID_CONFIGURATION
16from conans.client.api.conan_api import Conan
17from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError
18from conans.util.files import exception_message_safe
19from conans.util.log import logger
20
21
22class Cli(object):
23    """A single command of the conan application, with all the first level commands. Manages the
24    parsing of parameters and delegates functionality to the conan python api. It can also show the
25    help of the tool.
26    """
27
28    def __init__(self, conan_api):
29        assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format(
30            type(conan_api))
31        self._conan_api = conan_api
32        self._out = conan_api.out
33        self._groups = defaultdict(list)
34        self._commands = {}
35        conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands")
36        for module in pkgutil.iter_modules([conan_commands_path]):
37            module_name = module[1]
38            self._add_command("conans.cli.commands.{}".format(module_name), module_name)
39        user_commands_path = os.path.join(self._conan_api.cache_folder, "commands")
40        sys.path.append(user_commands_path)
41        for module in pkgutil.iter_modules([user_commands_path]):
42            module_name = module[1]
43            if module_name.startswith("cmd_"):
44                self._add_command(module_name, module_name.replace("cmd_", ""))
45
46    def _add_command(self, import_path, method_name):
47        try:
48            command_wrapper = getattr(importlib.import_module(import_path), method_name)
49            if command_wrapper.doc:
50                self._commands[command_wrapper.name] = command_wrapper
51                self._groups[command_wrapper.group].append(command_wrapper.name)
52            for name, value in getmembers(importlib.import_module(import_path)):
53                if isinstance(value, ConanSubCommand):
54                    if name.startswith("{}_".format(method_name)):
55                        command_wrapper.add_subcommand(value)
56                    else:
57                        raise ConanException("The name for the subcommand method should "
58                                             "begin with the main command name + '_'. "
59                                             "i.e. {}_<subcommand_name>".format(method_name))
60        except AttributeError:
61            raise ConanException("There is no {} method defined in {}".format(method_name,
62                                                                              import_path))
63
64    @property
65    def conan_api(self):
66        return self._conan_api
67
68    @property
69    def commands(self):
70        return self._commands
71
72    @property
73    def groups(self):
74        return self._groups
75
76    def _print_similar(self, command):
77        """ Looks for similar commands and prints them if found.
78        """
79        matches = get_close_matches(
80            word=command, possibilities=self.commands.keys(), n=5, cutoff=0.75)
81
82        if len(matches) == 0:
83            return
84
85        if len(matches) > 1:
86            self._out.info("The most similar commands are")
87        else:
88            self._out.info("The most similar command is")
89
90        for match in matches:
91            self._out.info("    %s" % match)
92
93        self._out.info("")
94
95    def help_message(self):
96        self.commands["help"].method(self.conan_api, self.commands["help"].parser,
97                                     commands=self.commands, groups=self.groups)
98
99    def run(self, *args):
100        """ Entry point for executing commands, dispatcher to class
101        methods
102        """
103        version = sys.version_info
104        if version.major == 2 or version.minor <= 4:
105            raise ConanException(
106                "Unsupported Python version. Minimum required version is Python 3.5")
107
108        try:
109            command_argument = args[0][0]
110        except IndexError:  # No parameters
111            self.help_message()
112            return SUCCESS
113        try:
114            command = self.commands[command_argument]
115        except KeyError as exc:
116            if command_argument in ["-v", "--version"]:
117                self._out.info("Conan version %s" % client_version)
118                return SUCCESS
119
120            if command_argument in ["-h", "--help"]:
121                self.help_message()
122                return SUCCESS
123
124            self._out.info("'%s' is not a Conan command. See 'conan --help'." % command_argument)
125            self._out.info("")
126            self._print_similar(command_argument)
127            raise ConanException("Unknown command %s" % str(exc))
128
129        command.run(self.conan_api, self.commands[command_argument].parser,
130                    args[0][1:], commands=self.commands, groups=self.groups)
131
132        return SUCCESS
133
134
135def cli_out_write(data, fg=None, bg=None):
136    data = "{}{}{}{}\n".format(fg or '', bg or '', data, Style.RESET_ALL)
137    sys.stdout.write(data)
138
139
140def main(args):
141    """ main entry point of the conan application, using a Command to
142    parse parameters
143
144    Exit codes for conan command:
145
146        0: Success (done)
147        1: General ConanException error (done)
148        2: Migration error
149        3: Ctrl+C
150        4: Ctrl+Break
151        5: SIGTERM
152        6: Invalid configuration (done)
153    """
154    try:
155        conan_api = Conan(quiet=False)
156    except ConanMigrationError:  # Error migrating
157        sys.exit(ERROR_MIGRATION)
158    except ConanException as e:
159        sys.stderr.write("Error in Conan initialization: {}".format(e))
160        sys.exit(ERROR_GENERAL)
161
162    def ctrl_c_handler(_, __):
163        print('You pressed Ctrl+C!')
164        sys.exit(USER_CTRL_C)
165
166    def sigterm_handler(_, __):
167        print('Received SIGTERM!')
168        sys.exit(ERROR_SIGTERM)
169
170    def ctrl_break_handler(_, __):
171        print('You pressed Ctrl+Break!')
172        sys.exit(USER_CTRL_BREAK)
173
174    signal.signal(signal.SIGINT, ctrl_c_handler)
175    signal.signal(signal.SIGTERM, sigterm_handler)
176
177    if sys.platform == 'win32':
178        signal.signal(signal.SIGBREAK, ctrl_break_handler)
179
180    try:
181        cli = Cli(conan_api)
182        exit_error = cli.run(args)
183    except SystemExit as exc:
184        if exc.code != 0:
185            logger.error(exc)
186            conan_api.out.error("Exiting with code: %d" % exc.code)
187        exit_error = exc.code
188    except ConanInvalidConfiguration as exc:
189        exit_error = ERROR_INVALID_CONFIGURATION
190        conan_api.out.error(exc)
191    except ConanException as exc:
192        exit_error = ERROR_GENERAL
193        conan_api.out.error(exc)
194    except Exception as exc:
195        import traceback
196        print(traceback.format_exc())
197        exit_error = ERROR_GENERAL
198        msg = exception_message_safe(exc)
199        conan_api.out.error(msg)
200
201    sys.exit(exit_error)
202