'''Parse file names and commands to extract file names.

Copyright 2000-2002 by Daniel Fandrich <dan@coneharvesters.com>

$Id: parsecommands.py,v 1.2 2003/02/14 16:21:49 dan Exp $
'''
from string import split, join, find, atoi, letters, digits
from os import getcwd
from pwd import getpwuid
from grp import getgrgid
from danutil import printerr
from fileanalyze import FileInfo
import posixpath
import getopt

#debuglog = printerr			# enable this for debug logs
def debuglog(errstr): pass		# enable this to stop debug logs

def betternormpath(name):
	'Normalize path, removing duplicated leading /'
	normname = posixpath.normpath(name)
	while normname[0:2] == '//':
		normname = normname[1:]
	return normname

def betterisabs(name):
	'See if a path is absolute, including relative to a specific user'
	return posixpath.isabs(name) or (name and name[0] == '~')

def abspath(name):
	'Make path absolute'
	if not posixpath.isabs(name):
		name = posixpath.expanduser(name)
	if not posixpath.isabs(name):
		name = betternormpath(posixpath.join(getcwd(),name))
	return betternormpath(name)

class ParseCommands:
	'Parse command-line style commands and extract file names'

	unsupportederrstr = "Warning: Unsupported command '%s'"

	PARSE_OK = None	# Method success return code
	PARSE_ERROR = 1	# Method error return code

	def __init__(self,filelist):
		self.filelist = filelist
		self.lastcommand = 'None'

	def printerr(self,message):
		printerr(message)

	def parse(self,command,args):
		self.lastcommand = command

		def safechars(ch):
			'Return the given character if valid for method name, else _'
			return (ch in letters+digits+'_' and ch) or '_'

		command = join(map(safechars, command),'')

		if hasattr(self, 'parse_' + command):
			method = getattr(self,'parse_' + command)
			return method(args)
		else:
			self.printerr(self.unsupportederrstr % command)
			return self.PARSE_ERROR


class ParseCommandLine(ParseCommands):
	'Subclass to handle command-line arguments'

	# Error messages
	unsupportederrstr = "Warning: Unsupported command '%s' in install script"
	argserrstr = 'Warning: invalid number of arguments to %s'
	argsopterrstr = 'Warning: invalid number of arguments or unsupported option to %s'

	# Support functions for this class

	def argserr(self):
		self.printerr(self.argserrstr % self.lastcommand)

	def argsopterr(self):
		self.printerr(self.argsopterrstr % self.lastcommand)

	def hasoption(self,args):
		'Returns nonzero if the first argument is an option'
		return args and args[0] and args[0][0] == '-'

	def multiplefiles(self, args, mode=None, owner=None, group=None):
		'''Handle potentially many files being copied (or whatever) to a single
		target'''

		sourcewild = 0
		if find(args[0], '*') >= 0 or find(args[0], '?') >= 0:
			sourcewild = 1		# destination must be a directory
		target = args[-1]

		if len(args) > 2 or sourcewild or self.filelist.finddir(target) \
		   or FileInfo(target).lookslikedir() or posixpath.isdir(target):
			# target is a directory
			# mark it as such in the file list
			debuglog('TARGET IS DIR: %s' % target)
			targetentry = self.filelist.find(target)
			if targetentry:
				targetentry.isdir = 1

			for source in args[:-1]:
				source = posixpath.basename(source)
				file = abspath(betternormpath(posixpath.join(target, source)))
				fileinfo = self.filelist.getentry(file)
				fileinfo.setattrib(mode=mode, owner=owner, group=group)
		else:
			# target is a file
			debuglog('TARGET IS FILE: %s' % target)
			target = betternormpath(target)
			fileinfo = self.filelist.getentry(target)
			fileinfo.setattrib(mode=mode, owner=owner, group=group)

	# Command parsing methods

	# Some commands are deliberately not processed:
	#  mv - not usually used to install, but sometimes used to make
	#       backups of configuration files
	#  sh - sometimes used to run a script to do more configuration
	#       or installation

	# The following commands are ignored
	def parse_true(self,args):
		'Ignore command'
		return self.PARSE_OK

	parse_cc = parse_true
	parse_gcc = parse_true
	parse_g__ = parse_true
	parse_ld = parse_true
	parse_yacc = parse_true
	parse_byacc = parse_true
	parse_bison = parse_true
	parse_lex = parse_true
	parse_flex = parse_true
	parse_cd = parse_true
	parse_ranlib = parse_true
	parse_strip = parse_true
	parse_rm = parse_true
	parse_gzip = parse_true
	parse_compress = parse_true
	parse_bzip2 = parse_true

	def parse_mkdir(self,args):
		if len(args) < 1:
			self.argserr()
			return self.PARSE_ERROR

		if args[0] == '-p':
			# ignore -p option
			args = args[1:]

		if len(args) != 1 or args[0][0] == '-':
			self.argserr()
			return self.PARSE_ERROR

		fileinfo = self.filelist.getentry(abspath(args[0]))
		fileinfo.setattrib(isdir=1)
		return self.PARSE_OK

	def parse_cp(self,args):
		if len(args) < 2:
			self.argserr()
			return self.PARSE_ERROR

		if args[0] == '-f':
			# ignore -f option
			args = args[1:]

		if len(args) < 2 or args[0][0] == '-':
			self.argserr()
			return self.PARSE_ERROR

		target = args[-1]
		if not posixpath.isabs(target):
			# Ignore copies of files into the current directory
			return self.PARSE_OK

		self.multiplefiles(args)
		return self.PARSE_OK

	def parse_install(self,args):
		if len(args) < 2:
			self.argserr()
			return self.PARSE_ERROR

		try:
			optlist, args = getopt.getopt(args, 'cdsg:m:o:')
		except getopt.error:
			self.printerr('Unsupported option given to install')
			return self.PARSE_ERROR

		# Look at all options
		installdir = 0
		mode = owner = group = None
		for opt,arg in optlist:
			if opt == '-c':
				# ignore
				pass
			elif opt == '-s':
				# ignore
				pass
			elif opt == '-m':
				# setting file mode
				mode = atoi(arg,8)
			elif opt == '-o':
				# setting file owner
				owner = arg
			elif opt == '-g':
				# setting file group
				group = arg
			elif opt == '-d':
				# create a directory
				installdir = 1

		if installdir:
			# Installing (creating) a directory
			if len(args) != 1:
				self.argserr()
				return self.PARSE_ERROR
			fileinfo = self.filelist.getentry(abspath(args[0]))
			fileinfo.setattrib(mode=mode,isdir=1)
			return self.PARSE_OK

		# Installing files
		if len(args) < 2:
			self.argserr()
			return self.PARSE_ERROR

		target = args[-1]

		if not posixpath.isabs(target):
			# Ignore copies of files into the current directory
			return self.PARSE_OK

		self.multiplefiles(args, mode, owner, group)
		return self.PARSE_OK

	# treat ginstall the same as install
	parse_ginstall = parse_install

	def parse_touch(self,args):
		if len(args) != 1 or self.hasoption(args):
			self.argsopterr()
			return self.PARSE_ERROR

		target = args[0]
		if not betterisabs(target):
			# Ignore touch of file in the current directory
			return self.PARSE_OK

		fileinfo = self.filelist.getentry(abspath(target))
		if FileInfo(target).lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)
		return self.PARSE_OK

	def parse_chown(self,args):
		if len(args) != 2 or self.hasoption(args):
			self.argsopterr()
			return self.PARSE_ERROR

		target = args[-1]
		if not betterisabs(target):
			# Ignore chown of file in the current directory
			return self.PARSE_OK
		target = abspath(target)

		fileinfo = self.filelist.getentry(target)
		if fileinfo.lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)
		# Split off group if specified
		owners = split(args[0],'.')
		if len(owners) == 1:
			# Maybe group is preceded by a colon instead of a period
			owners = split(args[0],':')
		fileinfo.owner = owners[0]
		if len(owners) > 1:
			fileinfo.group = owners[1]
		return self.PARSE_OK

	def parse_chgrp(self,args):
		if len(args) != 2 or self.hasoption(args):
			self.argsopterr()
			return self.PARSE_ERROR

		target = args[-1]
		if not betterisabs(target):
			# Ignore chgrp of file in the current directory
			return self.PARSE_OK
		target = abspath(target)

		fileinfo = self.filelist.getentry(target)
		if fileinfo.lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)
		fileinfo.group = args[0]
		return self.PARSE_OK

	def parse_chmod(self,args):
		if len(args) != 2 or self.hasoption(args):
			self.argsopterr()
			return self.PARSE_ERROR

		target = args[-1]
		if not betterisabs(target):
			# Ignore chmod of file in the current directory
			return self.PARSE_OK
		target = abspath(target)

		fileinfo = self.filelist.getentry(target)
		if fileinfo.lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)

		try:
			fileinfo.mode = atoi(args[0],8)
		except ValueError:
			self.printerr('Warning: Invalid mode specified in chmod')
			# continue without the changed mode
			return self.PARSE_ERROR
		return self.PARSE_OK

	def parse_ln(self,args):
		try:
			optlist, args = getopt.getopt(args, 'cdsg:m:o:')
		except getopt.error:
			self.argsopterr()
			return self.PARSE_ERROR

		# Look at all options
		for opt,arg in optlist:
			if opt == '-b':
				# ignore
				pass
			elif opt == '-f':
				# ignore
				pass
			elif opt == '-s':
				# ignore
				pass
			elif opt == '-v':
				# ignore
				pass

		if len(args) != 2:
			self.argserr()
			return self.PARSE_ERROR

		target = args[-1]
		if not betterisabs(target):
			# Ignore link of file in the current directory
			return self.PARSE_OK

		self.multiplefiles(args)
		return self.PARSE_OK

	def parse_echo(self,args):
		# This test for redirection is far from perfect; just look for > in arg
		if find(join(args),'>') >= 0:
			printerr('Warning: redirected echo not supported')
			return self.PARSE_ERROR
		# ignore echo otherwise
		return self.PARSE_OK

class ParseInstallwatch(ParseCommands):
	'Subclass to handle output of installwatch'

	# Error messages
	unsupportederrstr = "Warning: Unsupported command '%s' in installwatch log"

	# The following commands probably need to be supported, but we will need
	# to remember file descriptor history somehow
	# fchmod, fchown

	# The following commands are ignored
	def parse_true(self,args):
		'Ignore command'
		return self.PARSE_OK

	parse_chroot = parse_true
	parse_truncate = parse_true
	parse_ftruncate = parse_true
	parse_unlink = parse_true

	def parse_mkdir(self,args):
		fileinfo = self.filelist.getentry(abspath(args[1]))
		fileinfo.setattrib(isdir=1)
		return self.PARSE_OK

	def parse_open(self,args):
		target = args[1]
		if not posixpath.isabs(target):
			# Ignore copies of files into the current directory
			return self.PARSE_OK

		fileinfo = self.filelist.getentry(abspath(target))
		return self.PARSE_OK

	# Same parameters
	parse_creat = parse_open

	def parse_chmod(self,args):
		target = args[1]
		if not betterisabs(target):
			# Ignore chmod of file in the current directory
			return self.PARSE_OK
		target = abspath(target)

		fileinfo = self.filelist.getentry(target)
		if fileinfo.lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)

		try:
			fileinfo.mode = atoi(args[2],8) & 07777
		except ValueError:
			self.printerr('Warning: Invalid mode specified in chmod')
			# continue without the changed mode
			return self.PARSE_ERROR
		return self.PARSE_OK

	def parse_chown(self,args):
		target = args[1]
		if not betterisabs(target):
			# Ignore chown of file in the current directory
			return self.PARSE_OK
		target = abspath(target)

		fileinfo = self.filelist.getentry(target)
		if fileinfo.lookslikedir() or posixpath.isdir(target):
			fileinfo.setattrib(isdir=1)

		try:
			ownernum = atoi(args[2])
			groupnum = atoi(args[3])
		except ValueError:
			self.printerr('Warning: Invalid ID specified in chown')
			return self.PARSE_ERROR

		if ownernum != -1:
			try:
				fileinfo.owner = getpwuid(ownernum)[0]
			except KeyError:
				self.printerr('Warning: Invalid owner specified in chown')

		if groupnum != -1:
			try:
				fileinfo.group = getgrgid(groupnum)[0]
			except KeyError:
				self.printerr('Warning: Invalid group specified in chown')

		return self.PARSE_OK

	# Same parameters
	parse_lchown = parse_chown

	def parse_link(self,args):
		target = args[2]
		if not betterisabs(target):
			# Ignore link of file in the current directory
			return self.PARSE_OK

		# new file
		fileinfo = self.filelist.getentry(target)
		return self.PARSE_OK

	# Same parameters
	parse_symlink = parse_link

	def parse_rename(self,args):
		target = args[2]
		if not posixpath.isabs(target):
			# Ignore copies of files into the current directory
			return self.PARSE_OK

		fileinfo = self.filelist.getentry(target)
		return self.PARSE_OK

