aboutsummaryrefslogtreecommitdiff
blob: 513768361b26c6ecf799d5f046a83e3a5566622b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
#!/usr/bin/env python2
#
# -*- mode: python; -*-
#
# --| Version Information |------------------------------------------
# 
#  etcat v0.1.4 (27 Apr 2003)
#
#  $Header$
#
# --| About |--------------------------------------------------------
#
#  etcat is a Portage/Ebuild Information Extractor. Basically, it
#  provides higher level convienence functions to the Portage system
#  used by Gentoo Linux. 
#  
#  You can use it to quickly find out the recent changes of your
#  favourite package, the size of installed packages, the
#  available versions for a package and more.
#
# --| License |------------------------------------------------------
#
#  Distributed under the terms of the GNU General Public License v2
#  Copyright (c) 2002 Alastair Tse.
#
# --| Usage |--------------------------------------------------------
#
#  etcat [options] <command> <package[-ver]|ebuild|category/package[-ver]>
#
#  -b/belongs  ) checks what package(s) a file belongs to
#  -c/changes  ) list the more recent changelog entry
#  -d/depends  ) list all those that have this in their depends
#  -f/files	) list all files that belong to this package
#  -g/graph	) graph dependencies
#  -s/size	 ) guesses the size of a installed packaged.
#  -u/uses	 ) list all the use variables used in this package/ebuild
#  -v/versions ) list all the versions available for a package
#
# --| TODO |---------------------------------------------------------
# 
# - in ver_cmp: 1.2.10a < 1.2.10, need to fix that
# 
# --| Changes |------------------------------------------------------
#
#  * etcat-0.3.1 (10 Oct 2004) [karltk]
#	- Fixed changes help examples
#  * etcat-0.3.1 (08 Jan 2004) [genone]
#	- adding missing python searchpath modification
#	- fixing sort order
#  * etcat-0.3.0 (12 Jul 2003) [karltk]
#	- Refactored interesting stuff into the Gentoolkit module
#  * etcat-0.2.0 (13 Jun 2003)
#	- Updated "versions" with PORTAGE_OVERLAY detection
#	- Added "graph" feature
#  * etcat-0.1.5 (30 Apr 2003)
#	- Fixed disappearing short opts. Oops.
#  * etcat-0.1.4 (27 Apr 2003)
#	- Cleaned up command execution code to provide a single place
#	  to specify functions
#	- Added own custom wrapping print code.
#	- Added "files" feature
#	- Added "depends" feature
#  * etcat-0.1.3 (24 Apr 2003)
#	- Overhaul of commandline interpreter
#	- Added "belongs" feature
#	- Reimplemented "uses" to deal with IUSE more cleanly
#	  and sources use.local.desc
#	- Natural Order Listings for version
#  * etcat-0.1.2 (29 Mar 2003)
#	- Added unstable indicator to complement masked
#	- improved use flag listing
#  * etcat-0.1.1 (21 Jan 2003)
#	- Add package to versions listing even if it's not in
#	  the portage anymore (21 Jan 2003)
#	- Fixed old ehack references (17 Jan 2003)
#  * etcat-0.1 (31 Oct 2002)
#	Initial Release;
#
# -------------------------------------------------------------------



import os
import sys
import re
import pprint
import getopt
import glob

# portage and gentoolkit need special path modifications
sys.path.insert(0, "/usr/lib/portage/pym")
sys.path.insert(0, "/usr/lib/gentoolkit/pym")

import gentoolkit
from stat import *
try:
	from portage.output import *
except ImportError:
	from output import *

__author__ = "Alastair Tse"
__email__ = "liquidx@gentoo.org"
__version__ = "0.3.1"
__productname__ = "etcat"
__description__ = "Portage Information Extractor"

# .-------------------------------------------------------.
# | Initialise Colour Settings							|
# `-------------------------------------------------------'
if (not sys.stdout.isatty()) or (gentoolkit.settings["NOCOLOR"] in ["yes","true"]):
	nocolor()

# "option": ("shortcommand","desc",["example one", "example two"])
options = {
"belongs": \
("b","Searches for a package that owns a specified file with an option to restrict the search space.",
["etcat belongs /usr/bin/gimp media-gfx",
 "etcat belongs /usr/lib/libjpeg.so media-*",
 "etcat belongs /usr/lib/libmpeg.so"]),
"changes": \
("c","Outputs the changelog entry to screen. It is possible to give a version number along with the package name.",
["etcat changes mozilla", 
 "etcat changes =mozilla-1.1-r1", 
 "etcat changes gkrellm$"]),
"depends": \
("d","Finds all packages that are directly dependent to a regex search string.",
["etcat depends 'gnome-base/libgnome'", 
 "etcat depends '>=dev-lang/python-2.2'"]),
"files": \
("f","Lists files that belongs to a package and optionally with version.",[]),
"graph": \
("g","Graphs Dependencies (NON WORKING)",[]),
"size": \
("s","Lists the installed size of a package.",[]),
"uses": \
("u", "Advanced output of USE vars in a package. Tells you flags used by a package at time of installation, flags in current config and flag description.",[]),
"versions": \
("v","Displays the versions available for a specific package. Colour coded to indicate installation status and displays slot information.",
[turquoise("(I)") + "nstalled", 
 yellow("(~)") + "Unstable Testing Branch", 
 red("(M)") + "asked Package"])
}

# .-------------------------------------------------------.
# | Small Wrapping Printer with Indent Support			|
# `-------------------------------------------------------'

def wrap_print(string, indent=0, width=74):
	line_len = width - indent
	str_len = len(string)
	lines = []
	
	pos = 0
	thisline = ""
	while pos < str_len:
		# if we still have space stuff the
		# character in this line
		if len(thisline) < line_len-1:
			thisline += string[pos]
			pos += 1
		# if we're at the end of the line,
		# check if we should hyphenate or
		# append
		elif len(thisline) == line_len -1:
			# end of a text
			if pos == str_len -1:
				thisline += string[pos]
				pos += 1
			# end of a word
			elif string[pos] != " " and string[pos+1] == " ":
				thisline += string[pos]		
				pos += 1
			# just a space
			elif string[pos] == " ":
				thisline += string[pos]				
				pos += 1
			# start of a word, we start the word on the next line
			elif pos>0 and string[pos-1] == " ":
				thisline += " "
			# needs hyphenating
			else:
				thisline += "-"
				
			# append the line
			lines.append(thisline)
			thisline = ""

	# append last line
	if thisline:
		lines.append(thisline)
			
	for line in lines:
		print " "*indent + line

# +-------------------------------------------------------+
# | Pretty Print Log									  |
# +-------------------------------------------------------+
# | Extracts and prints out the log entry corresponding   |
# | to a certain revision if given. If not supplied,	  |
# | prints out the latest/topmost entry				   |
# `-------------------------------------------------------'	
	
# output the latest entry in changelog
def output_log(lines, package_ver=""):
	# using simple rules that all changelog entries have a "*"
	# as the first char
	is_log = 0
	is_printed = 0

	for line in lines:
		if package_ver:
			start_entry = re.search("^[\s]*\*[\s]*(" + package_ver + ")[\s]+.*(\(.*\))",line)
		else:
			start_entry = re.search("^[\s]*\*[\s]*(.*)[\s]+.*(\(.*\))",line)
		if not is_log and start_entry:
			is_printed = 1
			is_log = 1
			print green("*") + "  " + white(start_entry.group(1)) + "  " + turquoise(start_entry.group(2)) + " :"
		elif is_log and re.search("^[\s]*\*[\s]*(.*)[\s]+.*(\(.*\))",line):
			break
		elif is_log:
			print line.rstrip()
		else:
			pass
		
	return is_printed

# .-------------------------------------------------------.
# | Changes Function									  |
# +-------------------------------------------------------+
# | Print out the ChangeLog entry for package[-version]   |
# `-------------------------------------------------------'

def changes(query, matches):
	if not report_matches(query,matches,installed_only=0):
		return

	for pkg in matches:
		changelog_file = pkg.get_package_path() + "/ChangeLog"
		if os.path.exists(changelog_file):
			output_log(open(changelog_file).readlines(), pkg.get_name()+"-"+pkg.get_version())
		else:
			print red("Error") + ": No Changelog for " + pkg.get_cpv()


# .-------------------------------------------------------.
# | Versions Function									 |
# +-------------------------------------------------------+
# | Prints out the available version, masked status and   |
# | installed status.									 |
# `-------------------------------------------------------'		
		
def versions(query, matches):
	# this function should also report masked packages
	matches = gentoolkit.find_packages(query, masked=True)
	if not report_matches(query,matches):
		return

	# sorting result list
	matches = gentoolkit.sort_package_list(matches)
		
	# FIXME: old version printed result of regex search on name,
	# so "ant" would return app-emacs/semantic, etc...
	
	last_cp = ""

	for pkg in matches:
		new_cp = pkg.get_category()+"/"+pkg.get_name()
		if last_cp != new_cp:
			print green("*") + "  " + white(new_cp) + " :"
		last_cp = new_cp
			
		state = []
		color = green
		unstable = 0
		overlay = ""
			
		# check if masked
		if pkg.is_masked():
			state.append(red("M"))
			color = red
		else:
			state.append(" ")

		# check if in unstable
		kwd = pkg.get_env_var("KEYWORDS")
		if "~" + gentoolkit.settings["ARCH"] in kwd.split():
			state.append(yellow("~"))
			if color != red:
				color = yellow
			unstable = 1
		else:
			state.append(" ")
				
		# check if installed
		if pkg.is_installed():
			state.append(turquoise("I"))
			color = turquoise
		else:
			state.append(" ")

		# check if this is a OVERLAY ebuilds
		if pkg.is_overlay():
			overlay = " OVERLAY"

		ver = pkg.get_version()
		slot = pkg.get_env_var("SLOT")
		print " "*8 + "[" + "".join(state) + "] " + color(ver) + " (" + color(slot) + ")" + overlay

# .-------------------------------------------------------.
# | List USE flags for a single ebuild, if it's installed |
# +-------------------------------------------------------+
# | Just uses the new IUSE parameter in ebuilds		   |
# `-------------------------------------------------------' 
def uses(query, matches):
	useflags = gentoolkit.settings["USE"].split()	
	usedesc = {}
	uselocaldesc = {}

	# Load global USE flag descriptions
	try:
		fd = open(gentoolkit.settings["PORTDIR"]+"/profiles/use.desc")
		usedesc = {}
		for line in fd.readlines():
			if line[0] == "#":
				continue
			fields = line.split(" - ")
			if len(fields) == 2:
				usedesc[fields[0].strip()] = fields[1].strip()
	except IOError:
		pass

	# Load local USE flag descriptions
	try:
		fd = open(gentoolkit.settings["PORTDIR"]+"/profiles/use.local.desc")
		for line in fd.readlines():
			if line[0] == "#":
				continue
			fields = line.split(" - ")
			if len(fields) == 2:
				catpkguse = re.search("([a-z]+-[a-z]+/.*):(.*)", fields[0])
				if catpkguse:
					if not uselocaldesc.has_key(catpkguse.group(1).strip()):
						uselocaldesc[catpkguse.group(1).strip()] = {catpkguse.group(2).strip() : fields[1].strip()}
					else:
						uselocaldesc[catpkguse.group(1).strip()][catpkguse.group(2).strip()] = fields[1].strip()
	except IOError:
		pass
		
	print "[ Colour Code : " + green("set") + " " + red("unset") + " ]"
	print "[ Legend   : (U) Col 1 - Current USE flags        ]"
	print "[          : (I) Col 2 - Installed With USE flags ]"

	if filter(gentoolkit.Package.is_installed, matches):
		only_installed = True
	else:
		only_installed = False

	# Iterate through matches, printing a report for each package
	for p in matches:
		if not p.is_installed() and only_installed:
			continue
	
		bestver = p.get_cpv()
		iuse = p.get_env_var("IUSE")
		
		if iuse: usevar = iuse.split()
		else: usevar = []
		
		inuse = []
		used = p.get_use_flags().split()

		# store (inuse, inused, flag, desc)
		output = []

		for u in usevar:
			inuse = 0
			inused = 0
			try:
				desc = usedesc[u]
			except KeyError:
				try:
					desc = uselocaldesc[p.get_category()+"/"+p.get_name()][u]
				except KeyError:
					desc = ""

			if u in p.get_settings("USE"): inuse = 1
			if u in used: inused = 1
				
			output.append((inuse, inused, u, desc))

		# pretty print
		if output:
			print
			print white(" U I ") + "[ Found these USE variables in : " + white(bestver) + " ]"
			maxflag_len = 0
			for inuse, inused, u, desc in output:
				if len(u) > maxflag_len:
					maxflag_len = len(u)
		
			for inuse, inused, u, desc in output:
				flag = ["-","+"]
				colour = [red, green]
				if inuse != inused:
					print yellow(" %s %s" % (flag[inuse], flag[inused])),
				else:
					print " %s %s" % (flag[inuse], flag[inused]),

				print colour[inuse](u.ljust(maxflag_len)),
			
				# print description
				if desc:
					print ":", desc
				else:
					print ": unknown"
		else:
			print "[ No USE flags found for :", white(p.get_cpv()), "]"		

	return

# .-------------------------------------------------------.
# | Graphs the Dependency Tree for a package			  |
# +-------------------------------------------------------+
# | Naive graphing of dependencies
# `-------------------------------------------------------'

def graph(query, matches):
	if not report_matches(query, matches):
		return

	for pkg in matches:
		if not pkg.is_installed():
			continue
		rgraph(pkg)

def rgraph(pkg,level=0,pkgtbl=[],suffix=""):

	cpv=pkg.get_cpv()

	print level*" " + "`-- " + cpv + suffix
	pkgtbl.append(cpv)
	
	for x in pkg.get_runtime_deps():
		suffix=""
		cpv=x[2]
		pkg=gentoolkit.find_best_match(x[0] + cpv)
		if not pkg:
			continue
		if pkg.get_cpv() in pkgtbl:
			continue
		if cpv.find("virtual")==0:
			suffix+=" (" + cpv + ")"
		if len(x[1]):
			suffix+=" [ " + "".join(x[1]) + " ]"
		pkgtbl=rgraph(pkg,level+1,pkgtbl,suffix)
	return pkgtbl

# .-------------------------------------------------------.
# | Required By Function								  |
# +-------------------------------------------------------+
# | Find what packages require a given package name	   |
# `-------------------------------------------------------'

def depends(query, matches):
	
	print "[ Results for search key : " + white(query) + " ]"

	isdepend = gentoolkit.split_package_name(query)
	
	for pkg in matches:
		if pkg.is_installed():
			deps = pkg.get_runtime_deps()
			for x in deps:
				cpvs=gentoolkit.split_package_name(x[2])
				cat_match=0
				ver_match=0
				name_match=0
				if not isdepend[0] or isdepend[0] == cpvs[0]:
					cat_match=1
				if not isdepend[2] or \
				   (isdepend[2] == cpvs[2] and isdepend[3] == cpvs[3]):
					ver_match=1
				if isdepend[1] == cpvs[1]:
					name_match=1
				if cat_match and ver_match and name_match:
					print turquoise("*"), white(pkg.get_cpv()), white("[ ") + "".join(x[1]), white("]")

# .-------------------------------------------------------.
# | Belongs to which package							  |
# +-------------------------------------------------------+
# | Finds what package a file belongs to				  |
# `-------------------------------------------------------'			
			
def belongs(query,matches):

	q = query.split()

	if len(q) > 1:
		item=q[0]
		cat=q[1]
		fn=lambda x: x.find(cat)==0
	else:
		item=q[0]
		cat="*"
		fn=None
	matches = gentoolkit.find_all_installed_packages(fn)

	print "Searching for " + item + " in " + cat + " ..."

	rx = re.compile(item)

	for pkg in matches:
		if pkg.get_contents():
			for fn in pkg.get_contents().keys():
				if rx.search(fn):
					print pkg.get_cpv()
					break # We know this pkg matches, look for any more matches
	return
		
# .-------------------------------------------------------.
# | Size of all packages matching query				   |
# +-------------------------------------------------------+
# | Finds the size of installed packages				  |
# `-------------------------------------------------------'			
def size(query,packages):
	packages = gentoolkit.find_packages(query)
	if not report_matches(query, packages):
		return

	for pkg in packages:
		if not pkg.is_installed():
			continue
		x=pkg.size()
		size=x[0]
		files=x[1]
		uncounted=x[2]
		print turquoise("*") + " " + white(pkg.get_cpv())
		print " Total Files : ".rjust(25) + str(files)
		if uncounted:
			print " Inaccessible Files : ".rjust(25) + str(uncounted)
		print " Total Size : ".rjust(25) + "%.2f KB" % (size/1024.0)


def report_matches(query, matches, installed_only=1):
	print "[ Results for search key	   : " + white(query) + " ]"
	print "[ Candidate applications found : " + white(str(len(matches))) + " ]"
	print

	if installed_only and matches:
		print " Only printing found installed programs."
		print
	elif installed_only:
		print "No packages found."

	if matches:
		return 1
	else:
		return 0


# .-------------------------------------------------------.
# | Files in a package									|
# +-------------------------------------------------------+
# | Lists all the files in a package					  |
# `-------------------------------------------------------'			
def files(query,matches):
	if not report_matches(query, matches):
		return
	
	for package in matches:
		if not package.is_installed():
			continue
		contents = package.get_contents()
		
		print yellow(" * ") + white(package.get_cpv())
		for x in contents.keys():
			t = contents[x][0]
			if t == "obj":
				print x
			elif t == "sym":
				print turquoise(x)
			elif t == "dir":
				print blue(x)
			else:
				print x

# .-------------------------------------------------------.
# | Help Function										 |
# `-------------------------------------------------------'
def ver():
	print __productname__ + " (" + __version__ + ") - " + __description__ + " - By: " + __author__

def help():
	screenwidth = 74
	margin = 2
	margin_desc = 4
	margin_ex = 8
	
	ver()
	print yellow("NOTICE: ") + "This tool will be phased out at some point in"
	print "        the future, please use equery instead."
	print "        Bugs are still fixed, but new features won't be added."
	print
	print white("Usage: ") + turquoise(__productname__) + " [ " + green("options") + " ] [ " + turquoise("action") + " ] [ " + turquoise("package") + " ]"
	print
	print turquoise("Actions:")
	print
	for name,tup in options.items():
		print " "*margin + green(name) + " (" + green("-" + tup[0]) + " short option)"
		wrap_print(tup[1],indent=margin_desc)
		for example in tup[2]:
			print " "*margin_ex + example
		print

# .-------------------------------------------------------.
# | Main Function										 |
# `-------------------------------------------------------'
def main():
	
	action = ''
	query = ''
	
	if len(sys.argv) < 3:
		help()
		sys.exit(1)
		
	# delegates the commandline stuff to functions
	pointer = 2
	# short/long opts mapping
	shortopts = ["-"+x[0] for x in options.values()]
	short2long = {}
	for k,v in options.items():
		short2long[v[0]] = k
	longopts = options.keys()
	# loop thru arguments
	for arg in sys.argv[1:]:
		if arg[0] == "-" and len(arg) == 2 and arg in shortopts:
			action = short2long[arg[1]]
			query = ' '.join(sys.argv[pointer:])
			break
		elif arg in longopts:
			action = arg
			query = ' '.join(sys.argv[pointer:])
			break
		else:
			pointer += 1
			
	# abort if we don't have an action or query string
	if not query or action not in options.keys():
		help()
		sys.exit(1)
	else:
		try:
			matches = gentoolkit.find_packages(query)
		except KeyError, e:
			if e[0].find("Specific key requires operator") == 0:
				print red("!!!"), "Invalid syntax: missing operator"
				print red("!!!"), "If you want only specific versions please use one of"
				print red("!!!"), "the following operators as prefix for the package name:"
				print red("!!!"), "   >  >=  =  <=  <"
				print red("!!!"), "Example to only match gcc versions greater or equal 3.2:"
				print red("!!!"), "   >=sys-devel/gcc-3.2"
			else:
				print red("!!!"), "Internal portage error, terminating"
				if len(e[0]):
					print red("!!!"), e
			sys.exit(2)
		except ValueError, e:
			if isinstance(e[0],list):
				print red("!!!"), "Ambiguous package name \"%s\"" % query
				print red("!!!"), "Please use one of the following long names:"
				for p in e[0]:
					print red("!!!"), "    "+p
			else:
				print red("!!!"), "Internal portage error, terminating"
				if len(e[0]):
					print red("!!!"), e[0]
			sys.exit(2)
		function = globals()[action]
		function(query, matches)
	
if __name__ == "__main__":
	try:
		main()
	except KeyboardInterrupt:
		print "Operation Aborted!"