'''Dan's useful Python functions

Copyright 1996-2001 by Daniel Fandrich <dan@coneharvesters.com>

$Id: danutil.py,v 1.11 2001/11/09 23:09:27 dan Exp $
'''
# $Log: danutil.py,v $
# Revision 1.11  2001/11/09 23:09:27  dan
# Fixed bug in FileWrapper.fileno(). Now wrapping file object variables, too.
#
# Revision 1.10  2001/11/01 10:40:19  dan
# Fix argument quoting problem in editfile. Remove redundancy in fileexists.
#
# Revision 1.9  2001/03/30 03:31:21  dan
# Fixed default args in nullfile. Simplified span.
#
# Revision 1.8  2000/06/05 19:57:51  dan
# Added searchpath. Removed __del__ from FileWrapper. Improved FilesReader to
# support stdin.  Fixed SafeDict to give each caller a new empty dict.
#
# Revision 1.7  1999/06/01 04:04:05  dan
# Added FileWrapper class. Changed SafeDict to derive from UserDict.
# Added splitfirst(), stripcrlf(), quotedsplit(), fileexists()
#
# Revision 1.6  1999/02/27 23:09:56  dan
# Made SafeDict default to an empty dict.
# Changed splitfirst to return a tuple instead of a list.
#
# Revision 1.5  1999/02/26 07:26:45  dan
# Added span, splitfirst, SafeDict
#
# Revision 1.4  1998/07/14 04:04:40  dan
# Changed __version__ to return just the numeric RCS revision.
# 
# Revision 1.3  1998/06/12  20:21:33  dan
# Added InterleaveFiles
# 
# Revision 1.2  1998/06/12  20:16:43  dan
# Added Id and Log keywords.
# 
# 97-01-12
# 96-10-20 Dan Fandrich <dan@fch.wimsey.bc.ca>

#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2, or (at your option)
#  any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

# Some other commonly-needed functions that I've been tempted to add here
# but are already found elsewhere:
#  uniq: remove duplicates from a list -- lists.uniques
#  interleavelists: interleave two lists into a list of tuples -- lists.zip

import os
import sys
import stat
import string
import UserDict
import StringIO
from commands import mkarg

RCSrevision = "$Revision: 1.11 $"
__version__ = RCSrevision[11:-2]

NoSuchFile = 'no such file'

def findopen(filename, paths):
	'''Try to open a file for reading from somewhere in path

	file	file to open
	path	list of directories

	path is ignored if the filename is absolute
	Modified from try_open in ifile.py
	'''
        for dir in paths:
                tryfile = os.path.join(dir, filename)
                try:
                        return open(tryfile, 'r')
                except IOError:
                        pass
        raise NoSuchFile, filename

def searchpath(filename, path=None, filetype=None, mode=None):
	'''Search a path for a given file of a given type.
	If path is not specified, it defaults to os.environ['PATH']
	If filetype is not specified, it defaults to stat.S_IFREG
	If mode is not specified, it defaults to stat.S_IEXEC | stat.S_IXUSR |
	  stat.S_IXOTH
	'''
	if path==None:
		path = os.environ['PATH']
	if filetype==None:
		filetype = stat.S_IFREG
	if mode==None:
		mode = stat.S_IEXEC | stat.S_IXUSR | stat.S_IXOTH

	for component in string.split(path,os.pathsep):
		fn = os.path.join(component,filename)
		try:
			info = os.stat(fn)
			if (stat.S_IFMT(info[stat.ST_MODE]) == filetype) and \
				(stat.S_IMODE(info[stat.ST_MODE]) & mode):
				return fn
		except os.error:
			pass
	return ''

def printerr(msg):
	'''Print an error message to stderr including leading program name

	Displays a message of the form:
	 progname: This is an error or warning message
	'''
	sys.stderr.write('%s: %s\n' % (os.path.basename(sys.argv[0]), msg))
	
def getenviron(var, notdefined=None, isdefined=None):
	'''Return an environment variable or another value

	If notdefined and isdefined are not set, then the value of the 
	  environment variable var is returned, or None if it doesn't exist.
	If notdefined is given, then that value is returned instead of None
	  when the variable doesn't exist.
	If isdefined is given, then that value is returned instead of the
	  value of the variable when the variable exists.
	'''
	try:
		value = os.environ[var]
	except KeyError:
		return notdefined
	if isdefined:
		return isdefined
	return value

class nullfile:
	'''Null file sink

	Can be used in place of open('/dev/null') or StringIO
	'''
	def write(self,s):
		pass

	def flush(self):
		pass

	def read(self,n=-1):
		return ''

	def readline(self,n=-1):
		return ''

	def readlines(self):
		return ''

	def close(self):
		pass

	def isatty(self):
		return 0

	def seek(self, pos, mode = 0):
		return 0

class FileWrapper:
	'Emulates a file object, except for read and write methods'

	def __init__(self, file):
		self.file = file

	def __getattr__(self, attr):
		if hasattr(self,attr):
			return getattr(self,attr)
		return getattr(self.file,attr)

	def close(self):
		return self.file.close()

	def seek(self,pos):
		return self.file.seek(pos)

	def tell(self):
		return self.file.tell()

	def fileno(self):
		return self.file.fileno()

	def flush(self):
		return self.file.flush()

	def isatty(self):
		return self.file.isatty()

def editfile(fn):
	'''Invoke a text editor to edit the given file

	Tries to select an appropriate editor.
	Could be modified to guess at what editor the user likes
		- $EDITOR
		- look at dates of .exrc, .elvisrc, .emacs, .pinerc (pico)
		- verify that the executable exists
		- only choose X11 editor if $DISPLAY is set
		- vi, emacs, pico, grief, crisp, joe, jed, teco, uemacs

	'''
	editor = getenviron('EDITOR', notdefined='vi')
	return os.system(editor + ' ' + mkarg(fn))


class FilesReader:
    '''Read all lines from all files specified on the command line

	Use it like this:
		if __name__=="__main__":
		   for line in FilesReader():
			  sys.stdout.write(line) # or whatever you want...

	From: Aaron Watters <arw@big.att.com>
	Date: Wed, 03 Jul 1996 13:01:10 -0400
	Modified by Dan Fandrich to support reading from stdin

	This is similar to how Python 2.2 iterators work on files, where:
		for line in sys.stdin:
	works almost as well, but that construct doesn't use a command-line
	file name when given. 
	'''
    def __init__(self, list_of_files=sys.argv[1:]):
       self.list = list_of_files
       if self.list:
          self.thefile = open(list_of_files[0])
       else:
          self.thefile = sys.stdin

    def __getitem__(self, ignore):
       line = self.thefile.readline()
       if line: return line
       else:
          self.thefile.close()
          self.list = self.list[1:]
          try:
              self.thefile = open(self.list[0])
          except IndexError:
              raise IndexError, "no more lines"
          else:
              return self.__getitem__(ignore)

class InterleaveFiles:
	"Interleave lines of input from two files"
	def __init__(self, file1, file2):
		self.file1 = file1
		self.file2 = file2
		self.currentfile = 1
		self.mismatched = 0
	def ismismatched(self):
		"""Whether the two files have differing numbers of lines
		Only valid immediately after calling readlines()
		"""
		return self.mismatched
	def close(self):
		self.file1.close()
		self.file2.close()
	def readlines(self):
		lines = []
		while 1:
			l = self.readline()
			if not l:
				# If this is not the first file that reached the end, or
				# if there are more lines in the second file, lengths are
				# mismatched.
				if self.currentfile != 1 or self.readline():
					# More lines in other file--mismatched lengths
					self.mismatched = 1
				break
			lines.append(l)
		return lines

	def readline(self):
		if self.currentfile == 1:
			self.currentfile = 2
			return self.file1.readline()
		else:
			self.currentfile = 1
			return self.file2.readline()

def span(str,accept):
	'''Returns the length of the initial portion of str consisting entirely
	of characters in accept. Like the C function strspn()
	'''
	for i in range(len(str)):
		if not str[i] in accept:
			return i
	return len(str)

class SafeDict(UserDict.UserDict):
	'Get a key from a dict without throwing an exception on a nonexistent key'
	def __init__(self,dict=None,notdefined=None):
		'''dict is the dictionary with which to populate the class
		notdefined is the value returned when a key does not exist'''
		if not dict:
			dict = {}	# Create mutable dict at run-time, not load time
		UserDict.UserDict.__init__(self)
		self.notdefined = notdefined
		self.data = dict

	def __getitem__(self,key):
		if self.data.has_key(key):
			return self.data[key]
		return self.notdefined

def splitfirst(str,sep=' ',num=1):
	'''Split a string into two or more fields, separated by sep
	Returns a tuple of the fields. If too many fields are specified,
	an empty string is returned for that element of the tuple.
	In Python >= 1.4, this function is very similar to:
		return tuple(string.split(str,sep,num))
	except that if num > number of seps, we return blanks
	'''
	if num < 0:
	  num = 0
	pieces = []
	for index in range(num):
		pos = string.find(str,sep)
		if pos < 0: # sep not found
			pos = len(str)
		pieces = pieces + [str[:pos]]
		str = str[pos+1:]
	pieces = pieces + [str]
	return tuple(pieces)

def stripcrlf(str):
	'Strip CR and LF characters from the end of a string'
	while str and str[-1] in ['\n','\r']:
			str=str[:-1]
	return str

def quotedsplit(str,splitchars=string.whitespace,openquote='"',closequote='"',
		repeatedsplitchars=1,doubledquotes=1,keepquotes=0):
	'''Split a string into fields, honouring quotes
	To do: create options for handling embedded \ quoted quotes;
	  - come up with better names for the options
	  - create option to treat splitchars as a single field
	'''
	linefile = StringIO.StringIO(str)
	fields = []
	field = ''
	while 1:
		char = linefile.read(1)
		if not char:
			break
		if char in openquote:
			# Quote mode
			if keepquotes:
				field = field + char
			while 1:
				char = linefile.read(1)
				if not char:
					break
				if char in closequote:
					if keepquotes:
						field = field + char
					# See if this is a doubled quote
					char = linefile.read(1)
					if char:
						if doubledquotes and char in closequote:
							field = field + char
						else:
							linefile.seek(linefile.tell()-1)
							# end quote mode
							break
					else:	# end of string
						break
				else:
					# Character within quotes
					field = field + char
		elif char in splitchars:
			if repeatedsplitchars:
				# treat subsequent splitchars as one
				while char and char in splitchars:
					char = linefile.read(1)
				if char:
					linefile.seek(linefile.tell()-1)

			fields.append(field)
			field = ''
		else:
			# Character outside quotes
			field = field + char
	# Put the final field into the list
	fields.append(field)
	return tuple(fields)

def fileexists(fn):
	"Returns TRUE if the file exists, FALSE if it does not or could not tell"
	try:
		os.stat(fn)
		return 1	# file exists
	except os.error:
		return 0	# file does not exist or is not readable

