Fix equery's term_width too.
[gentoolkit.git] / pym / gentoolkit / equery / __init__.py
1 # Copyright(c) 2009, Gentoo Foundation
2 #
3 # Licensed under the GNU General Public License, v2
4 #
5 # $Header: $
6
7 """Gentoo package query tool"""
8
9 from __future__ import print_function
10
11 __all__ = (
12         'format_options',
13         'format_package_names',
14         'mod_usage'
15 )
16 __docformat__ = 'epytext'
17 # version is dynamically set by distutils sdist
18 __version__ = "svn"
19
20 # =======
21 # Imports
22 # =======
23
24 import errno
25 import os
26 import sys
27 import time
28 from getopt import getopt, GetoptError
29
30 import portage
31
32 import gentoolkit
33 from gentoolkit import CONFIG
34 from gentoolkit import errors
35 from gentoolkit import pprinter as pp
36 from gentoolkit.textwrap_ import TextWrapper
37
38 __productname__ = "equery"
39 __authors__ = (
40         'Karl Trygve Kalleberg - Original author',
41         'Douglas Anderson - 0.3.0 author'
42 )
43
44 # =======
45 # Globals
46 # =======
47
48 NAME_MAP = {
49         'b': 'belongs',
50         'c': 'changes',
51         'k': 'check',
52         'd': 'depends',
53         'g': 'depgraph',
54         'f': 'files',
55         'h': 'hasuse',
56         'l': 'list_',
57         'y': 'keywords',
58         'a': 'has',
59         'm': 'meta',
60         's': 'size',
61         'u': 'uses',
62         'w': 'which'
63 }
64
65 # =========
66 # Functions
67 # =========
68
69 def print_help(with_description=True):
70         """Print description, usage and a detailed help message.
71
72         @param with_description (bool): Option to print module's __doc__ or not
73         """
74
75         if with_description:
76                 print(__doc__)
77         print(main_usage())
78         print()
79         print(pp.globaloption("global options"))
80         print(format_options((
81                 (" -h, --help", "display this help message"),
82                 (" -q, --quiet", "minimal output"),
83                 (" -C, --no-color", "turn off colors"),
84                 (" -N, --no-pipe", "turn off pipe detection"),
85                 (" -V, --version", "display version info")
86         )))
87         print()
88         print(pp.command("modules") + " (" + pp.command("short name") + ")")
89         print(format_options((
90                 (" (b)elongs", "list what package FILES belong to"),
91                 (" (c)hanges", "list changelog entries for ATOM"),
92                 (" chec(k)", "verify checksums and timestamps for PKG"),
93                 (" (d)epends", "list all packages directly depending on ATOM"),
94                 (" dep(g)raph", "display a tree of all dependencies for PKG"),
95                 (" (f)iles", "list all files installed by PKG"),
96                 (" h(a)s", "list all packages for matching ENVIRONMENT data stored in /var/db/pkg"),
97                 (" (h)asuse", "list all packages that have USE flag"),
98                 (" ke(y)words", "display keywords for specified PKG"),
99                 (" (l)ist", "list package matching PKG"),
100                 (" (m)eta", "display metadata about PKG"),
101                 (" (s)ize", "display total size of all files owned by PKG"),
102                 (" (u)ses", "display USE flags for PKG"),
103                 (" (w)hich", "print full path to ebuild for PKG")
104         )))
105
106
107 def expand_module_name(module_name):
108         """Returns one of the values of NAME_MAP or raises KeyError"""
109
110         if module_name == 'list':
111                 # list is a Python builtin type, so we must rename our module
112                 return 'list_'
113         elif module_name in NAME_MAP.values():
114                 return module_name
115         else:
116                 return NAME_MAP[module_name]
117
118
119 def format_options(options):
120         """Format module options.
121
122         @type options: list
123         @param options: [('option 1', 'description 1'), ('option 2', 'des... )]
124         @rtype: str
125         @return: formatted options string
126         """
127
128         result = []
129         twrap = TextWrapper(width=CONFIG['termWidth'])
130         opts = (x[0] for x in options)
131         descs = (x[1] for x in options)
132         for opt, desc in zip(opts, descs):
133                 twrap.initial_indent = pp.emph(opt.ljust(25))
134                 twrap.subsequent_indent = " " * 25
135                 result.append(twrap.fill(desc))
136
137         return '\n'.join(result)
138
139
140 def format_filetype(path, fdesc, show_type=False, show_md5=False,
141                 show_timestamp=False):
142         """Format a path for printing.
143
144         @type path: str
145         @param path: the path
146         @type fdesc: list
147         @param fdesc: [file_type, timestamp, MD5 sum/symlink target]
148                 file_type is one of dev, dir, obj, sym.
149                 If file_type is dir, there is no timestamp or MD5 sum.
150                 If file_type is sym, fdesc[2] is the target of the symlink.
151         @type show_type: bool
152         @param show_type: if True, prepend the file's type to the formatted string
153         @type show_md5: bool
154         @param show_md5: if True, append MD5 sum to the formatted string
155         @type show_timestamp: bool
156         @param show_timestamp: if True, append time-of-creation after pathname
157         @rtype: str
158         @return: formatted pathname with optional added information
159         """
160
161         ftype = fpath = stamp = md5sum = ""
162
163         if fdesc[0] == "obj":
164                 ftype = "file"
165                 fpath = path
166                 stamp = format_timestamp(fdesc[1])
167                 md5sum = fdesc[2]
168         elif fdesc[0] == "dir":
169                 ftype = "dir"
170                 fpath = pp.path(path)
171         elif fdesc[0] == "sym":
172                 ftype = "sym"
173                 stamp = format_timestamp(fdesc[1])
174                 tgt = fdesc[2].split()[0]
175                 if CONFIG["piping"]:
176                         fpath = path
177                 else:
178                         fpath = pp.path_symlink(path + " -> " + tgt)
179         elif fdesc[0] == "dev":
180                 ftype = "dev"
181                 fpath = path
182         else:
183                 sys.stderr.write(
184                         pp.error("%s has unknown type: %s" % (path, fdesc[0]))
185                 )
186
187         result = ""
188         if show_type:
189                 result += "%4s " % ftype
190         result += fpath
191         if show_timestamp:
192                 result += "  " + stamp
193         if show_md5:
194                 result += "  " + md5sum
195
196         return result
197
198
199 def format_timestamp(timestamp):
200         """Format a timestamp into, e.g., '2009-01-31 21:19:44' format"""
201
202         return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(timestamp)))
203
204
205 def initialize_configuration():
206         """Setup the standard equery config"""
207
208         # Get terminal size
209         term_width = pp.output.get_term_size()[1]
210         if term_width < 1:
211                 # get_term_size() failed. Set a sane default width:
212                 term_width = 80
213
214         # Terminal size, minus a 1-char margin for text wrapping
215         CONFIG['termWidth'] = term_width - 1
216
217         # Guess color output
218         if (CONFIG['color'] == -1 and (not sys.stdout.isatty() or
219                 os.getenv("NOCOLOR") in ("yes", "true")) or CONFIG['color'] == 0):
220                 pp.output.nocolor()
221
222         if CONFIG['piping']:
223                 CONFIG['verbose'] = False
224                 # set extra wide, should disable wrapping unless 
225                 # there is some extra long text 
226                 CONFIG['termWidth'] = 600
227
228         CONFIG['debug'] = bool(os.getenv('DEBUG', False))
229
230
231 def main_usage():
232         """Return the main usage message for equery"""
233
234         return "%(usage)s %(product)s [%(g_opts)s] %(mod_name)s [%(mod_opts)s]" % {
235                 'usage': pp.emph("Usage:"),
236                 'product': pp.productname(__productname__),
237                 'g_opts': pp.globaloption("global-options"),
238                 'mod_name': pp.command("module-name"),
239                 'mod_opts': pp.localoption("module-options")
240         }
241
242
243 def mod_usage(mod_name="module", arg="pkgspec", optional=False):
244         """Provide a consistent usage message to the calling module.
245
246         @type arg: string
247         @param arg: what kind of argument the module takes (pkgspec, filename, etc)
248         @type optional: bool
249         @param optional: is the argument optional?
250         """
251
252         return "%(usage)s: %(mod_name)s [%(opts)s] %(arg)s" % {
253                 'usage': pp.emph("Usage"),
254                 'mod_name': pp.command(mod_name),
255                 'opts': pp.localoption("options"),
256                 'arg': ("[%s]" % pp.emph(arg)) if optional else pp.emph(arg)
257         }
258
259
260 def parse_global_options(global_opts, args):
261         """Parse global input args and return True if we should display help for
262         the called module, else False (or display help and exit from here).
263         """
264
265         need_help = False
266         opts = (opt[0] for opt in global_opts)
267         for opt in opts:
268                 if opt in ('-h', '--help'):
269                         if args:
270                                 need_help = True
271                         else:
272                                 print_help()
273                                 sys.exit(0)
274                 elif opt in ('-q','--quiet'):
275                         CONFIG['quiet'] = True
276                 elif opt in ('-C', '--no-color', '--nocolor'):
277                         CONFIG['color'] = 0
278                         pp.output.nocolor()
279                 elif opt in ('-N', '--no-pipe'):
280                         CONFIG['piping'] = False
281                 elif opt in ('-V', '--version'):
282                         print_version()
283                         sys.exit(0)
284                 elif opt in ('--debug'):
285                         CONFIG['debug'] = True
286
287         return need_help
288
289
290 def print_version():
291         """Print the version of this tool to the console."""
292
293         print("%(product)s (%(version)s) - %(docstring)s" % {
294                 "product": pp.productname(__productname__),
295                 "version": __version__,
296                 "docstring": __doc__
297         })
298
299
300 def split_arguments(args):
301         """Separate module name from module arguments"""
302
303         return args.pop(0), args
304
305
306 def main():
307         """Parse input and run the program."""
308
309         short_opts = "hqCNV"
310         long_opts = (
311                 'help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version', 'debug'
312         )
313
314         initialize_configuration()
315
316         try:
317                 global_opts, args = getopt(sys.argv[1:], short_opts, long_opts)
318         except GetoptError as err:
319                 sys.stderr.write(pp.error("Global %s" % err))
320                 print_help(with_description=False)
321                 sys.exit(2)
322
323         # Parse global options
324         need_help = parse_global_options(global_opts, args)
325
326         # verbose is shorthand for the very common 'not quiet or piping'
327         if CONFIG['quiet'] or CONFIG['piping']:
328                 CONFIG['verbose'] = False
329         else:
330                 CONFIG['verbose'] = True
331
332         try:
333                 module_name, module_args = split_arguments(args)
334         except IndexError:
335                 print_help()
336                 sys.exit(2)
337
338         if need_help:
339                 module_args.append('--help')
340
341         try:
342                 expanded_module_name = expand_module_name(module_name)
343         except KeyError:
344                 sys.stderr.write(pp.error("Unknown module '%s'" % module_name))
345                 print_help(with_description=False)
346                 sys.exit(2)
347
348         try:
349                 loaded_module = __import__(
350                         expanded_module_name, globals(), locals(), [], -1
351                 )
352                 loaded_module.main(module_args)
353         except portage.exception.AmbiguousPackageName as err:
354                 raise errors.GentoolkitAmbiguousPackage(err.args[0])
355         except IOError as err:
356                 if err.errno != errno.EPIPE:
357                         raise
358
359 if __name__ == '__main__':
360         main()