sambatool: heuristics to decided whether colour is wanted
[kai/samba-autobuild/.git] / python / samba / netcmd / __init__.py
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009-2012
3 # Copyright (C) Theresa Halloran <theresahalloran@gmail.com> 2011
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 import optparse, samba
20 from samba import getopt as options
21 from samba import colour
22 from ldb import LdbError
23 import sys, traceback
24 import textwrap
25
26 class Option(optparse.Option):
27     pass
28
29 # This help formatter does text wrapping and preserves newlines
30 class PlainHelpFormatter(optparse.IndentedHelpFormatter):
31     def format_description(self,description=""):
32             desc_width = self.width - self.current_indent
33             indent = " "*self.current_indent
34             paragraphs = description.split('\n')
35             wrapped_paragraphs = [
36                 textwrap.fill(p,
37                         desc_width,
38                         initial_indent=indent,
39                         subsequent_indent=indent)
40                 for p in paragraphs]
41             result = "\n".join(wrapped_paragraphs) + "\n"
42             return result
43
44     def format_epilog(self, epilog):
45         if epilog:
46             return "\n" + epilog + "\n"
47         else:
48             return ""
49
50 class Command(object):
51     """A samba-tool command."""
52
53     def _get_short_description(self):
54         return self.__doc__.splitlines()[0].rstrip("\n")
55
56     short_description = property(_get_short_description)
57
58     def _get_full_description(self):
59         lines = self.__doc__.split("\n")
60         return lines[0] + "\n" + textwrap.dedent("\n".join(lines[1:]))
61
62     full_description = property(_get_full_description)
63
64     def _get_name(self):
65         name = self.__class__.__name__
66         if name.startswith("cmd_"):
67             return name[4:]
68         return name
69
70     name = property(_get_name)
71
72     # synopsis must be defined in all subclasses in order to provide the
73     # command usage
74     synopsis = None
75     takes_args = []
76     takes_options = []
77     takes_optiongroups = {}
78
79     hidden = False
80
81     raw_argv = None
82     raw_args = None
83     raw_kwargs = None
84
85     def __init__(self, outf=sys.stdout, errf=sys.stderr):
86         self.outf = outf
87         self.errf = errf
88
89     def usage(self, prog, *args):
90         parser, _ = self._create_parser(prog)
91         parser.print_usage()
92
93     def show_command_error(self, e):
94         '''display a command error'''
95         if isinstance(e, CommandError):
96             (etype, evalue, etraceback) = e.exception_info
97             inner_exception = e.inner_exception
98             message = e.message
99             force_traceback = False
100         else:
101             (etype, evalue, etraceback) = sys.exc_info()
102             inner_exception = e
103             message = "uncaught exception"
104             force_traceback = True
105
106         if isinstance(inner_exception, LdbError):
107             (ldb_ecode, ldb_emsg) = inner_exception.args
108             self.errf.write("ERROR(ldb): %s - %s\n" % (message, ldb_emsg))
109         elif isinstance(inner_exception, AssertionError):
110             self.errf.write("ERROR(assert): %s\n" % message)
111             force_traceback = True
112         elif isinstance(inner_exception, RuntimeError):
113             self.errf.write("ERROR(runtime): %s - %s\n" % (message, evalue))
114         elif type(inner_exception) is Exception:
115             self.errf.write("ERROR(exception): %s - %s\n" % (message, evalue))
116             force_traceback = True
117         elif inner_exception is None:
118             self.errf.write("ERROR: %s\n" % (message))
119         else:
120             self.errf.write("ERROR(%s): %s - %s\n" % (str(etype), message, evalue))
121             force_traceback = True
122
123         if force_traceback or samba.get_debug_level() >= 3:
124             traceback.print_tb(etraceback, file=self.errf)
125
126     def _create_parser(self, prog, epilog=None):
127         parser = optparse.OptionParser(
128             usage=self.synopsis,
129             description=self.full_description,
130             formatter=PlainHelpFormatter(),
131             prog=prog,epilog=epilog)
132         parser.add_options(self.takes_options)
133         optiongroups = {}
134         for name, optiongroup in self.takes_optiongroups.items():
135             optiongroups[name] = optiongroup(parser)
136             parser.add_option_group(optiongroups[name])
137         return parser, optiongroups
138
139     def message(self, text):
140         self.outf.write(text+"\n")
141
142     def _run(self, *argv):
143         parser, optiongroups = self._create_parser(argv[0])
144         opts, args = parser.parse_args(list(argv))
145         # Filter out options from option groups
146         args = args[1:]
147         kwargs = dict(opts.__dict__)
148         for option_group in parser.option_groups:
149             for option in option_group.option_list:
150                 if option.dest is not None:
151                     del kwargs[option.dest]
152         kwargs.update(optiongroups)
153
154         # Check for a min a max number of allowed arguments, whenever possible
155         # The suffix "?" means zero or one occurence
156         # The suffix "+" means at least one occurence
157         # The suffix "*" means zero or more occurences
158         min_args = 0
159         max_args = 0
160         undetermined_max_args = False
161         for i, arg in enumerate(self.takes_args):
162             if arg[-1] != "?" and arg[-1] != "*":
163                min_args += 1
164             if arg[-1] == "+" or arg[-1] == "*":
165                undetermined_max_args = True
166             else:
167                max_args += 1
168         if (len(args) < min_args) or (not undetermined_max_args and len(args) > max_args):
169             parser.print_usage()
170             return -1
171
172         self.raw_argv = list(argv)
173         self.raw_args = args
174         self.raw_kwargs = kwargs
175
176         try:
177             return self.run(*args, **kwargs)
178         except Exception as e:
179             self.show_command_error(e)
180             return -1
181
182     def run(self):
183         """Run the command. This should be overridden by all subclasses."""
184         raise NotImplementedError(self.run)
185
186     def get_logger(self, name="netcmd"):
187         """Get a logger object."""
188         import logging
189         logger = logging.getLogger(name)
190         logger.addHandler(logging.StreamHandler(self.errf))
191         return logger
192
193     def apply_colour_choice(self, requested):
194         """Heuristics to work out whether the user wants colour output, from a
195         --color=yes|no|auto option. This alters the ANSI 16 bit colour
196         "constants" in the colour module to be either real colours or empty
197         strings.
198         """
199         requested = requested.lower()
200         if requested == 'no':
201             colour.switch_colour_off()
202
203         elif requested == 'yes':
204             colour.switch_colour_on()
205
206         elif requested == 'auto':
207             if (hasattr(self.outf, 'isatty') and self.outf.isatty()):
208                 colour.switch_colour_on()
209             else:
210                 colour.switch_colour_off()
211
212         else:
213             raise CommandError("Unknown --color option: %s "
214                                "please choose from yes, no, auto")
215
216
217 class SuperCommand(Command):
218     """A samba-tool command with subcommands."""
219
220     synopsis = "%prog <subcommand>"
221
222     subcommands = {}
223
224     def _run(self, myname, subcommand=None, *args):
225         if subcommand in self.subcommands:
226             return self.subcommands[subcommand]._run(
227                 "%s %s" % (myname, subcommand), *args)
228
229         if subcommand == 'help':
230             # pass the request down
231             if len(args) > 0:
232                 sub = self.subcommands.get(args[0])
233                 if isinstance(sub, SuperCommand):
234                     return sub._run("%s %s" % (myname, args[0]), 'help',
235                                     *args[1:])
236                 elif sub is not None:
237                     return sub._run("%s %s" % (myname, args[0]), '--help',
238                                     *args[1:])
239
240             subcommand = '--help'
241
242         epilog = "\nAvailable subcommands:\n"
243         subcmds = self.subcommands.keys()
244         subcmds.sort()
245         max_length = max([len(c) for c in subcmds])
246         for cmd_name in subcmds:
247             cmd = self.subcommands[cmd_name]
248             if not cmd.hidden:
249                 epilog += "  %*s  - %s\n" % (
250                     -max_length, cmd_name, cmd.short_description)
251         epilog += "For more help on a specific subcommand, please type: %s <subcommand> (-h|--help)\n" % myname
252
253         parser, optiongroups = self._create_parser(myname, epilog=epilog)
254         args_list = list(args)
255         if subcommand:
256             args_list.insert(0, subcommand)
257         opts, args = parser.parse_args(args_list)
258
259         parser.print_help()
260         return -1
261
262
263 class CommandError(Exception):
264     """An exception class for samba-tool Command errors."""
265
266     def __init__(self, message, inner_exception=None):
267         self.message = message
268         self.inner_exception = inner_exception
269         self.exception_info = sys.exc_info()
270
271     def __repr__(self):
272         return "CommandError(%s)" % self.message