#!/usr/bin/env python

import commands
import locale
import os
import re
import sys
import tempfile
import textwrap

from optparse import OptionParser
from optparse import make_option
from urlparse import urljoin

try:
	import readline
except ImportError:
	readline = None

from bugzilla import Bugz
from config import config

BUGZ_COMMENT_TEMPLATE = \
"""
BUGZ: ---------------------------------------------------
%s
BUGZ: Any line beginning with 'BUGZ:' will be ignored.
BUGZ: ---------------------------------------------------
"""

DEFAULT_NUM_COLS = 80

#
# Auxiliary functions
#

def raw_input_block():
	""" Allows multiple line input until a Ctrl+D is detected.

	@rtype: string
	"""
	target = ''
	while True:
		try:
			line = raw_input()
			target += line + '\n'
		except EOFError:
			return target

#
# This function was lifted from Bazaar 1.9.
#
def terminal_width():
	"""Return estimated terminal width."""
	if sys.platform == 'win32':
		return win32utils.get_console_size()[0]
	width = DEFAULT_NUM_COLS
	try:
		import struct, fcntl, termios
		s = struct.pack('HHHH', 0, 0, 0, 0)
		x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
		width = struct.unpack('HHHH', x)[1]
	except IOError:
		pass
	if width <= 0:
		try:
			width = int(os.environ['COLUMNS'])
		except:
			pass
	if width <= 0:
		width = DEFAULT_NUM_COLS

	return width

def launch_editor(initial_text, comment_from = '',comment_prefix = 'BUGZ:'):
	"""Launch an editor with some default text.

	Lifted from Mercurial 0.9.
	@rtype: string
	"""
	(fd, name) = tempfile.mkstemp("bugz")
	f = os.fdopen(fd, "w")
	f.write(comment_from)
	f.write(initial_text)
	f.close()

	editor = (os.environ.get("BUGZ_EDITOR") or
			os.environ.get("EDITOR"))
	if editor:
		result = os.system("%s \"%s\"" % (editor, name))
		if result != 0:
			raise RuntimeError('Unable to launch editor: %s' % editor)

		new_text = open(name).read()
		new_text = re.sub('(?m)^%s.*\n' % comment_prefix, '', new_text)
		os.unlink(name)
		return new_text

	return ''

def block_edit(comment, comment_from = ''):
	editor = (os.environ.get('BUGZ_EDITOR') or
			os.environ.get('EDITOR'))

	if not editor:
		print comment + ': (Press Ctrl+D to end)'
		new_text = raw_input_block()
		return new_text

	initial_text = '\n'.join(['BUGZ: %s'%line for line in comment.split('\n')])
	new_text = launch_editor(BUGZ_COMMENT_TEMPLATE % initial_text, comment_from)

	if new_text.strip():
		return new_text
	else:
		return ''

#
# Bugz specific exceptions
#

class BugzError(Exception):
	pass

#
# ugly optparse callbacks (really need to integrate this somehow)
#

def modify_opt_fixed(opt, opt_str, val, parser):
	parser.values.status = 'RESOLVED'
	parser.values.resolution = 'FIXED'

def modify_opt_invalid(opt, opt_str, val, parser):
	parser.values.status = 'RESOLVED'
	parser.values.resolution = 'INVALID'

class PrettyBugz(Bugz):
	options = {
		'base': make_option('-b', '--base', type='string',
							default = 'https://bugs.gentoo.org/',
							help = 'Base URL of Bugzilla'),
		'user': make_option('-u', '--user', type='string',
							help = 'Username for commands requiring authentication'),
		'password': make_option('-p', '--password', type='string',
							help = 'Password for commands requiring authentication'),
		'httpuser': make_option('-H', '--httpuser', type='string',
							help = 'Username for basic http auth'),
		'httppassword': make_option('-P', '--httppassword', type='string',
							help = 'Password for basic http auth'),
		'forget': make_option('-f', '--forget', action='store_true',
							help = 'Forget login after execution'),
		'columns': make_option('--columns', type='int', default = 0,
							help = 'Maximum number of columns output should use'),
		'encoding': make_option('--encoding',
							help = 'Output encoding (default: utf-8).'),
		'skip_auth': make_option('--skip-auth', action='store_true',
							default = False, help = 'Skip Authentication.'),
		'quiet': make_option('-q', '--quiet', action='store_true',
							default = False, help = 'Quiet mode'),
	}

	def __init__(self, base, user = None, password =None, forget = False,
			columns = 0, encoding = '', skip_auth = False,
			quiet = False, httpuser = None, httppassword = None ):

		self.quiet = quiet
		self.columns = columns or terminal_width()

		Bugz.__init__(self, base, user, password, forget, skip_auth, httpuser, httppassword)

		self.log("Using %s " % self.base)

		if not encoding:
			try:
				self.enc = locale.getdefaultlocale()[1]
			except:
				self.enc = 'utf-8'

			if not self.enc:
				self.enc = 'utf-8'
		else:
			self.enc = encoding

	def log(self, status_msg, newline = True):
		if not self.quiet:
			if newline:
				print ' * %s' % status_msg
			else:
				print ' * %s' % status_msg,

	def warn(self, warn_msg):
		if not self.quiet:
			print ' ! Warning: %s' % warn_msg

	def get_input(self, prompt):
		return raw_input(prompt)

	def search(self, *args, **kwds):
		"""Performs a search on the bugzilla database with the keywords given on the title (or the body if specified).
		"""
		search_term = ' '.join(args).strip()
		show_status = kwds['show_status']
		del kwds['show_status']
		show_url = kwds['show_url']
		del kwds['show_url']
		search_opts = sorted([(opt, val) for opt, val in kwds.items()
			if val != None and opt != 'order'])

		if not (search_term or search_opts):
			raise BugzError('Please give search terms or options.')

		if search_term:
			log_msg = 'Searching for \'%s\' ' % search_term
		else:
			log_msg = 'Searching for bugs '

		if search_opts:
			self.log(log_msg + 'with the following options:')
			for opt, val in search_opts:
				self.log('   %-20s = %s' % (opt, val))
		else:
			self.log(log_msg)

		result = Bugz.search(self, search_term, **kwds)

		if result == None:
			raise RuntimeError('Failed to perform search')

		if len(result) == 0:
			self.log('No bugs found.')
			return

		self.listbugs(result, show_url, show_status)

	search.args = "<search term> [options..]"
	search.options = {
		'order': make_option('-o', '--order', type='choice',
								choices = config.choices['order'].keys(),
								default = 'number'),
		'assigned_to': make_option('-a', '--assigned-to',
								help = 'email the bug is assigned to'),
		'reporter': make_option('-r', '--reporter',
								help = 'email the bug was reported by'),
		'cc': make_option('--cc',help = 'Restrict by CC email address'),
		'commenter': make_option('--commenter',help = 'email that commented the bug'),
		'status': make_option('-s', '--status', action='append',
								help = 'Bug status (for multiple choices,'
								'use --status=NEW --status=ASSIGNED) or --status=all for all statuses'),
		'severity': make_option('--severity', action='append',
								choices = config.choices['severity'],
								help = 'Restrict by severity.'),
		'priority': make_option('--priority', action='append',
								choices = config.choices['priority'].values(),
								help = 'Restrict by priority (1 or more)'),
		'comments': make_option('-c', '--comments',  action='store_true',
								help = 'Search comments instead of title'),
		'product': make_option('--product', action='append',
								help = 'Restrict by product (1 or more)'),
		'component': make_option('-C', '--component', action='append',
								help = 'Restrict by component (1 or more)'),
		'keywords': make_option('-k', '--keywords', help = 'Bug keywords'),
		'whiteboard': make_option('-w', '--whiteboard',
								help = 'Status whiteboard'),
		'show_status': make_option('--show-status', help='show status of bugs',
								action = 'store_true', default = False),
		'show_url': make_option('--show-url', help='Show bug id as a url.',
								action = 'store_true', default = False),
	}

	def namedcmd(self, command, show_status=False, show_url=False):
		"""Run a command stored in Bugzilla by name."""
		log_msg = 'Running namedcmd \'%s\''%command
		result = Bugz.namedcmd(self, command)
		if result == None:
			raise RuntimeError('Failed to run command\nWrong namedcmd perhaps?')

		if len(result) == 0:
			self.log('No result from command')
			return

		self.listbugs(result, show_url, show_status)

	namedcmd.args = "<command name>"
	namedcmd.options = {
		'show_status': make_option('--show-status', help='show status of bugs',
								action = 'store_true', default = False),
		'show_url': make_option('--show-url', help='Show bug id as a url.',
								action = 'store_true', default = False),
	}

	def get(self, bugid, comments = True, attachments = True):
		""" Fetch bug details given the bug id """
		self.log('Getting bug %s ..' % bugid)

		result = Bugz.get(self, bugid)

		if result == None:
			raise RuntimeError('Bug %s not found' % bugid)

		# Print out all the fields below by extract the text
		# directly from the tag, and just ignore if we don't
		# see the tag.
		FIELDS = (
			('short_desc', 'Title'),
			('assigned_to', 'Assignee'),
			('creation_ts', 'Reported'),
			('delta_ts', 'Updated'),
			('bug_status', 'Status'),
			('resolution', 'Resolution'),
			('bug_file_loc', 'URL'),
			('bug_severity', 'Severity'),
			('priority', 'Priority'),
			('reporter', 'Reporter'),
		)

		MORE_FIELDS = (
			('product', 'Product'),
			('component', 'Component'),
			('status_whiteboard', 'Whiteboard'),
			('keywords', 'Keywords'),
		)

		for field, name in FIELDS + MORE_FIELDS:
			try:
				value = result.find('//%s' % field).text
			except AttributeError:
				continue
			print '%-12s: %s' % (name, value.encode(self.enc))

		# Print out the cc'ed people
		cced = result.findall('.//cc')
		for cc in cced:
			print '%-12s: %s' %  ('CC', cc.text)

		# print out depends
		dependson = ', '.join([d.text for d in result.findall('.//dependson')])
		blocked = ', '.join([d.text for d in result.findall('.//blocked')])
		if dependson:
			print '%-12s: %s' % ('DependsOn', dependson)
		if blocked:
			print '%-12s: %s' % ('Blocked', blocked)

		bug_comments = result.findall('//long_desc')
		bug_attachments = result.findall('//attachment')

		print '%-12s: %d' % ('Comments', len(bug_comments))
		print '%-12s: %d' % ('Attachments', len(bug_attachments))
		print '%-12s: %s' % ('URL', '%s?id=%s' % (urljoin(self.base,
													config.urls['show']),
													bugid))
		print

		if attachments:
			for attachment in bug_attachments:
				aid = attachment.find('.//attachid').text
				desc = attachment.find('.//desc').text
				when = attachment.find('.//date').text
				print '[Attachment] [%s] [%s]' % (aid, desc.encode(self.enc))

		if comments:
			i = 0
			wrapper = textwrap.TextWrapper(width = self.columns)
			for comment in bug_comments:
				try:
					who = comment.find('.//who').text.encode(self.enc)
				except AttributeError:
					# Novell doesn't use 'who' on xml
					who = ""
				when = comment.find('.//bug_when').text.encode(self.enc)
				what =  comment.find('.//thetext').text
				print '\n[Comment #%d] %s : %s'  % (i, who, when)
				print '-' * (self.columns - 1)

				if what is None:
					what = ''

				# print wrapped version
				for line in what.split('\n'):
					if len(line) < self.columns:
						print line.encode(self.enc)
					else:
						for shortline in wrapper.wrap(line):
							print shortline.encode(self.enc)
				i += 1
			print

	get.args = "<bug_id> [options..]"
	get.options = {
		'comments': make_option("-n", "--no-comments", dest = 'comments',
								action="store_false", default = True,
								help = 'Do not show comments'),
	}

	def post(self, product = None, component = None,
			title = None, description = None, assigned_to = None,
			cc = None, url = None, keywords = None,
			description_from = None, prodversion = None, append_command = None,
			dependson = None, blocked = None, batch = False,
			default_confirm = 'y', priority = None, severity = None):
		"""Post a new bug"""

		# load description from file if possible
		if description_from:
			try:
				description = open(description_from, 'r').read()
			except IOError, e:
				raise BugzError('Unable to read from file: %s: %s' % \
								(description_from, e))

		if not batch:
			self.log('Press Ctrl+C at any time to abort.')

			#
			#  Check all bug fields.
			#  XXX: We use "if not <field>" for mandatory fields
			#       and "if <field> is None" for optional ones.
			#

			# check for product
			if not product:
				while not product or len(product) < 1:
					product = self.get_input('Enter product: ')
			else:
				self.log('Enter product: %s' % product)

			# check for version
			# FIXME: This default behaviour is not too nice.
			if prodversion is None:
				prodversion = self.get_input('Enter version (default: unspecified): ')
			else:
				self.log('Enter version: %s' % prodversion)

			# check for component
			if not component:
				while not component or len(component) < 1:
					component = self.get_input('Enter component: ')
			else:
				self.log('Enter component: %s' % component)

			# check for default priority
			if priority is None:
				priority_msg ='Enter priority (eg. P2) (optional): '
				priority = self.get_input(priority_msg)
			else:
				self.log('Enter priority (optional): %s' % priority)

			# check for default severity
			if severity is None:
				severity_msg ='Enter severity (eg. normal) (optional): '
				severity = self.get_input(severity_msg)
			else:
				self.log('Enter severity (optional): %s' % severity)

			# check for default assignee
			if assigned_to is None:
				assigned_msg ='Enter assignee (eg. liquidx@gentoo.org) (optional): '
				assigned_to = self.get_input(assigned_msg)
			else:
				self.log('Enter assignee (optional): %s' % assigned_to)

			# check for CC list
			if cc is None:
				cc_msg = 'Enter a CC list (comma separated) (optional): '
				cc = self.get_input(cc_msg)
			else:
				self.log('Enter a CC list (optional): %s' % cc)

			# check for optional URL
			if url is None:
				url = self.get_input('Enter URL (optional): ')
			else:
				self.log('Enter URL (optional): %s' % url)

			# check for title
			if not title:
				while not title or len(title) < 1:
					title = self.get_input('Enter title: ')
			else:
				self.log('Enter title: %s' % title)

			# check for description
			if not description:
				description = block_edit('Enter bug description: ')
			else:
				self.log('Enter bug description: %s' % description)

			if append_command is None:
				append_command = self.get_input('Append the output of the following command (leave blank for none): ')
			else:
				self.log('Append command (optional): %s' % append_command)

			# check for Keywords list
			if keywords is None:
				kwd_msg = 'Enter a Keywords list (comma separated) (optional): '
				keywords = self.get_input(kwd_msg)
			else:
				self.log('Enter a Keywords list (optional): %s' % keywords)

			# check for bug dependencies
			if dependson is None:
				dependson_msg = 'Enter a list of bug dependencies (comma separated) (optional): '
				dependson = self.get_input(dependson_msg)
			else:
				self.log('Enter a list of bug dependencies (optional): %s' % dependson)

			# check for blocker bugs
			if blocked is None:
				blocked_msg = 'Enter a list of blocker bugs (comma separated) (optional): '
				blocked = self.get_input(blocked_msg)
			else:
				self.log('Enter a list of blocker bugs (optional): %s' % blocked)

		# append the output from append_command to the description
		if append_command is not None and append_command != '':
			append_command_output = commands.getoutput(append_command)
			description = description + '\n\n' + '$ ' + append_command + '\n' +  append_command_output

		# raise an exception if mandatory fields are not specified.
		if product is None:
			raise RuntimeError('Product not specified')
		if component is None:
			raise RuntimeError('Component not specified')
		if title is None:
			raise RuntimeError('Title not specified')
		if description is None:
			raise RuntimeError('Description not specified')

		# set optional fields to their defaults if they are not set.
		if prodversion is None:
			prodversion = ''
		if priority is None:
			priority = ''
		if severity is None:
			severity = ''
		if assigned_to is None:
			assigned_to = ''
		if cc is None:
			cc = ''
		if url is None:
			url = ''
		if keywords is None:
			keywords = ''
		if dependson is None:
			dependson = ''
		if blocked is None:
			blocked = ''

		# print submission confirmation
		print '-' * (self.columns - 1)
		print 'Product     : ' + product
		print 'Version     : ' + prodversion
		print 'Component   : ' + component
		print 'priority    : ' + priority
		print 'severity    : ' + severity
		print 'Assigned to : ' + assigned_to
		print 'CC          : ' + cc
		print 'URL         : ' + url
		print 'Title       : ' + title
		print 'Description : ' + description
		print 'Keywords    : ' + keywords
		print 'Depends on  : ' + dependson
		print 'Blocks      : ' + blocked
		print '-' * (self.columns - 1)

		if not batch:
			if default_confirm in ['Y','y']:
				confirm = raw_input('Confirm bug submission (Y/n)? ')
			else:
				confirm = raw_input('Confirm bug submission (y/N)? ')
			if len(confirm) < 1:
				confirm = default_confirm
			if confirm[0] not in ('y', 'Y'):
				self.log('Submission aborted')
				return

		result = Bugz.post(self, product, component, title, description, url, assigned_to, cc, keywords, prodversion, dependson, blocked, priority, severity)
		if result != None and result != 0:
			self.log('Bug %d submitted' % result)
		else:
			raise RuntimeError('Failed to submit bug')

	post.args = "[options]"
	post.options = {
		'product': make_option('--product', help = 'Product'),
		'component': make_option('--component', help = 'Component'),
		'prodversion': make_option('--prodversion',
									help = 'Version of the product'),
		'title': make_option('-t', '--title', help = 'Title of bug'),
		'description': make_option('-d', '--description',
									help = 'Description of the bug'),
		'description_from': make_option('-F' , '--description-from',
									help = 'Description from contents of'
									' file'),
		'append_command': make_option('--append-command',
									help = 'Append the output of a command to the description.'),
		'assigned_to': make_option('-a', '--assigned-to',
									help = 'Assign bug to someone other than '
									'the default assignee'),
		'cc': make_option('--cc', help = 'Add a list of emails to CC list'),
		'url': make_option('-U', '--url',
									help = 'URL associated with the bug'),
		'dependson': make_option('--depends-on', dest='dependson', help = 'Add a list of bug dependencies'),
		'blocked': make_option('--blocked', help = 'Add a list of blocker bugs'),
		'keywords': make_option('-k', '--keywords', help = 'List of bugzilla keywords'),
		'batch': make_option('--batch', action="store_true",
									help = 'do not prompt for any values'),
		'default_confirm': make_option('--default-confirm',
									choices = ['y','Y','n','N'],
									default = 'y',
									help = 'default answer to confirmation question (y/n)'),
		'priority': make_option('--priority',
								choices=config.choices['priority'].values()),
		'severity': make_option('-S', '--severity',
								choices=config.choices['severity']),
	}


	def modify(self, bugid, **kwds):
		"""Modify an existing bug (eg. adding a comment or changing resolution.)"""
		if 'comment_from' in kwds:
			if kwds['comment_from']:
				try:
					kwds['comment']  = open(kwds['comment_from'], 'r').read()
				except IOError, e:
					raise BugzError('Failed to get read from file: %s: %s' % \
									(comment_from, e))

				if 'comment_editor' in kwds:
					if kwds['comment_editor']:
						kwds['comment'] = block_edit('Enter comment:', kwds['comment'])
						del kwds['comment_editor']

			del kwds['comment_from']

		if 'comment_editor' in kwds:
			if kwds['comment_editor']:
				kwds['comment'] = block_edit('Enter comment:')
			del kwds['comment_editor']

		result = Bugz.modify(self, bugid, **kwds)
		if not result:
			raise RuntimeError('Failed to modify bug')
		else:
			self.log('Modified bug %s with the following fields:' % bugid)
			for field, value in result:
				self.log('  %-12s: %s' % (field, value))


	modify.args = "<bug_id> [options..]"
	modify.options = {
		'title': make_option('-t', '--title', help = 'Set title of bug'),
		'comment_from': make_option('-F', '--comment-from',
									help = 'Add comment from file.  If -C is also specified, the editor will be opened with this file as its contents.'),
		'comment_editor': make_option('-C', '--comment-editor',
									action='store_true', default = False,
									help = 'Add comment via default editor'),
		'comment': make_option('-c', '--comment', help = 'Add comment to bug'),
		'url': make_option('-U', '--url', help = 'Set URL field of bug'),
		'status': make_option('-s', '--status',
									choices=config.choices['status'].values(),
									help = 'Set new status of bug (eg. RESOLVED)'),
		'resolution': make_option('-r', '--resolution',
									choices=config.choices['resolution'].values(),
									help = 'Set new resolution (only if status = RESOLVED)'),
		'assigned_to': make_option('-a', '--assigned-to'),
		'duplicate': make_option('-d', '--duplicate', type='int', default=0),
		'priority': make_option('--priority',
								choices=config.choices['priority'].values()),
		'severity': make_option('-S', '--severity',
								choices=config.choices['severity']),
		'fixed': make_option('--fixed', action='callback',
								callback = modify_opt_fixed,
								help = "Mark bug as RESOLVED, FIXED"),
		'invalid': make_option('--invalid', action='callback',
								callback = modify_opt_invalid,
								help = "Mark bug as RESOLVED, INVALID"),
		'add_cc': make_option('--add-cc', action = 'append',
								help = 'Add an email to the CC list'),
		'remove_cc': make_option('--remove-cc', action = 'append',
								help = 'Remove an email from the CC list'),
		'add_dependson': make_option('--add-dependson', action = 'append',
								help = 'Add a bug to the depends list'),
		'remove_dependson': make_option('--remove-dependson', action = 'append',
								help = 'Remove a bug from the depends list'),
		'add_blocked': make_option('--add-blocked', action = 'append',
								help = 'Add a bug to the blocked list'),
		'remove_blocked': make_option('--remove-blocked', action = 'append',
								help = 'Remove a bug from the blocked list'),
		'whiteboard': make_option('-w', '--whiteboard',
								help = 'Set Status whiteboard'),
		'keywords': make_option('-k', '--keywords',
								help = 'Set bug keywords'),
		}

	def attachment(self, attachid, view = False):
		""" Download or view an attachment given the id."""
		self.log('Getting attachment %s' % attachid)

		result = Bugz.attachment(self, attachid)
		if not result:
			raise RuntimeError('Unable to get attachment')

		action = {True:'Viewing', False:'Saving'}
		self.log('%s attachment: "%s"' % (action[view], result['filename']))
		safe_filename = os.path.basename(re.sub(r'\.\.', '',
												result['filename']))

		if view:
			print result['fd'].read()
		else:
			if os.path.exists(result['filename']):
				raise RuntimeError('Filename already exists')

			open(safe_filename, 'wb').write(result['fd'].read())

	attachment.args = "<attachid> [-v]"
	attachment.options = {
		'view': make_option('-v', '--view', action="store_true",
							default = False,
							help = "Print attachment rather than save")
	}

	def attach(self, bugid, filename, content_type = 'text/plain', description = None):
		""" Attach a file to a bug given a filename. """
		if not os.path.exists(filename):
			raise BugzError('File not found: %s' % filename)
		if not description:
			description = block_edit('Enter description (optional)')
		result = Bugz.attach(self, bugid, filename, description, filename,
				content_type)

	attach.args = "<bugid> <filename> [-c=<mimetype>] [-d=<description>]"
	attach.options = {
		'content_type': make_option('-c', '--content-type',
									default='text/plain',
									help = 'Mimetype of the file (default: text/plain)'),
		'description': make_option('-d', '--description',
									help = 'A description of the attachment.')
	}

	def listbugs(self, buglist, show_url=False, show_status=False):
		for row in buglist:
			bugid = row['bugid']
			if show_url:
				bugid = '%s%s?id=%s'%(self.base, config.urls['show'], bugid)
			status = row['status']
			desc = row['desc']
			line = '%s' % (bugid)
			if show_status:
				line = '%s %s' % (line, status)
			if row.has_key('assignee'): # Novell does not have 'assignee' field
				assignee = row['assignee'].split('@')[0]
				line = '%s %-20s' % (line, assignee)

			line = '%s %s' % (line, desc)

			try:
				print line.encode(self.enc)[:self.columns]
			except UnicodeDecodeError:
				print line[:self.columns]

		self.log("%i bug(s) found." % len(buglist))

	@classmethod
	def help(self):
		print 'Usage: bugz <subcommand> [parameter(s)] [options..]'
		print
		print 'Subcommands:'
		print '  search      Search for bugs in bugzilla'
		print '  get         Get a bug from bugzilla'
		print '  attachment  Get an attachment from bugzilla'
		print '  post        Post a new bug into bugzilla'
		print '  modify      Modify a bug (eg. post a comment)'
		print '  attach      Attach file to a bug'
		print '  namedcmd    Run a stored search,'
		print
		print 'Examples:'
		print '  bugz get 12345'
		print '  bugz search python --assigned-to liquidx@gentoo.org'
		print '  bugz attachment 5000 --view'
		print '  bugz attach 140574 python-2.4.3.ebuild'
		print '  bugz modify 140574 -c "Me too"'
		print '  bugz namedcmd "Amd64 stable"'
		print
		print 'For more information on subcommands, run:'
		print '  bugz <subcommand> --help'
