#! /usr/bin/env python

#    cfv - Command-line File Verify
#    Copyright (C) 2000-2001  Matthew Mueller <donut@azstarnet.com>
#
#    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 of the License, 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import getopt, re, os, string, sys, errno, time

types={}

def chomp(line):
	if line[-2:] == '\r\n': return line[:-2]
	elif line[-1:] in '\r\n': return line[:-1]
	return line


curdir=os.getcwd()
def chdir(d):
	global curdir
	os.chdir(d)
	curdir=os.getcwd()

def p(s,nl=1,outf=sys.stdout):
	outf.write(s)
	if nl:
		outf.write('\n')
def pverbose(s,nl=1):
	if config.verbose>0:
		p(s,nl)
def pinfo(s,nl=1):
	if config.verbose>=0:
		p(s,nl)
def perror(s,nl=1):
	if config.verbose>=-1:
		p(s,nl,outf=sys.stderr)

class CFVException(Exception):
	pass

class CFVValueError(CFVException):#invalid argument in user input
	pass

class CFVNameError(CFVException):#invalid command in user input
	pass

class CFVSyntaxError(CFVException):#error in user input
	pass

class CFVVerifyError(CFVException):#invalid crc/filesize/etc
	pass

class Stats:
	num=0
	ok=0
	badsize=0
	badcrc=0
	notfound=0
	ferror=0
	cferror=0
	bytesread=0L #use long int for size, to avoid possible overflow.
	unverifiedfiles=[]
	unverified=0

class Config:
	verbose=0 # -1=quiet  0=norm  1=noisy
	docrcchecks=1
	dirsort=1
	cmdlinesort=1
	recursive=0
	showunverified=0
	defaulttype='sfv'
	ignorecase=0
	fixpaths=''
	showpaths=2
	gzip=0
	rename=0
	renameformat='%(name)s.bad-%(count)i%(ext)s'
	renameformatnocount=0
	def setdefault(self,type):
		if type in types.keys():
			self.defaulttype=type
		else:
			raise CFVValueError, "invalid default type '%s'"%type
	def setintr(self,o,v,min,max):
		try:
			x=int(v)
			if x>max or x<min:
				raise CFVValueError, "out of range int '%s' for %s"%(v,o)
			self.__dict__[o]=x
		except ValueError:
			raise CFVValueError, "invalid int type '%s' for %s"%(v,o)
	def setbool(self,o,v):
		v=string.lower(v)
		if v in ('yes','on','true','1'):
			x=1
		elif v in ('no','off','false','0'):
			x=0
		else:
			raise CFVValueError, "invalid bool type '%s' for %s"%(v,o)
		self.__dict__[o]=x
	def setstr(self,o,v):
		self.__dict__[o]=v
	def setx(self,o,v):
		if o=="default":
			self.setdefault(v)
		elif o in ("dirsort","cmdlinesort","showunverified","ignorecase","rename"):
			self.setbool(o,v)
		elif o in ("verbose","gzip"):
			self.setintr(o,v,-1,1)
		elif o in ("recursive","showpaths"):
			self.setintr(o,v,0,2)
		elif o=="fixpaths":
			self.setstr(o,v)
		elif o=="renameformat":
			testmap=make_rename_formatmap('1.2')
			testmapwc=make_rename_formatmap('1.2')
			testmapwc['count']=1
			format_test=v%testmapwc
			try:
				format_test=v%testmap
				self.renameformatnocount=1 #if we can get here, it doesn't use the count param
			except KeyError:
				self.renameformatnocount=0
			self.renameformat=v
		else:
			raise CFVNameError, "invalid option '%s'"%o
	def readconfig(self):
		filename=os.path.expanduser("~/.cfvrc")
		if os.path.isfile(filename):
			file=open(filename,"r")
			l=0
			while 1:
				l=l+1
				s=file.readline()
				if not len(s):
					break #end of file
				if s[0]=="#":
					continue #ignore lines starting with #
				#s=re.sub('[\n\r]','',s)
				s=chomp(s)
				if not len(s):
					continue #ignore blank lines
				x=re.search("^(.*) (.*)$",s)
				if not x:
					raise CFVSyntaxError, "%s:%i: invalid line '%s'"%(filename,l,s)
				else:
					o,v=x.groups()
					try:
						self.setx(o,v)
					except CFVException, err:
						raise sys.exc_info()[0], "%s:%i: %s"%(filename,l,err), sys.exc_info()[2] #reuse the traceback of the original exception, but add file and line numbers to the error
	def __init__(self):
		self.readconfig()
			
			
def make_rename_formatmap(l_filename):
	sp=os.path.splitext(l_filename)
	return {'name':sp[0], 'ext':sp[1], 'fullname':l_filename}

version='1.8'#-pre'+'$Revision: 1.23 $'[11:-2]

try:
	import fchksum
	try:
		if fchksum.version()<2:raise ImportError
	except:
		p("old fchksum version installed, using std python modules. please update.",nl=1,outf=sys.stderr) #can't use perror yet since config hasn't been done..
		raise ImportError
	def getfilemd5(file):
		c,s=fchksum.fmd5t(file)
		stats.bytesread=stats.bytesread+s
		return c,s
	def getfilecrc(file):
		c,s=fchksum.fcrc32t(file)
		stats.bytesread=stats.bytesread+s
		return c,s
except ImportError:
	import md5,zlib
	def getfilemd5(file):
		m=md5.new()
		if file=='':
			f=sys.stdin
		else:
			f=open(file,'rb')
		s=0L
		while 1:
			x=f.read(65536)
			if not len(x):
				c=""
				for z in m.digest():
					c=c+'%02x'%ord(z)
				stats.bytesread=stats.bytesread+s
				return c,s
			s=s+len(x)
			m.update(x)
	def getfilecrc(file):
		if file=='':
			f=sys.stdin
		else:
			f=open(file,'rb')
		c=zlib.crc32('')
		s=0L
		while 1:
			x=f.read(65536)
			if not len(x):
				stats.bytesread=stats.bytesread+s
				return "%08X"%c,s
			s=s+len(x)
			c=zlib.crc32(x,c)

try:
	import filecmp
	def fcmp(f1, f2):
		return filecmp.cmp(f1, f2, shallow=0)
except ImportError:
	import cmp #obsolete since python 1.6
	def fcmp(f1, f2):
		#old cmp module is always shallow, so call its real do_cmp function if the files signatures match.  do_cmp is undocumented but in all the versions of the cmp.py on sourceforge python cvs so I think its safe :)
		return cmp.cmp(f1, f2) and cmp.do_cmp(f1, f2)


class fv_type:
	def __init__(self,csf,tm,am,msf,wh,mdf):
#		self.checksumfile=csf
		self.dochecksumfileline=csf
		self.testmatch=tm
		self.automake_filenametest=am
		self.make_std_filename=msf
		self.writeheader=wh
		self.make_dofile=mdf

class PeekFile:
	"a (very) limited file-like class that gets around having to seek back after reading a line"
	def __init__(self, fileobj, doclose=1):
		self.fobj=fileobj
		self.peeked=''
		self.doclose=doclose
		#speed up by not even checking peek status normally
		self.readline=self.fobj.readline #take advantage of the fact that we only use size arg with peekline
		self.write=self.fobj.write
		self.flush=self.fobj.flush
	def close(self):
		if self.doclose:
			self.fobj.close()
		else:
			self.flush()
		self.fobj=None
	def hackedreadline(self, size=-1):
		try:
			return self.fobj.readline(size)
		except TypeError:
			return self.fobj.readline() #hack for broken readline() in older gzip.py versions
	def peekline(self, size=-1):
		if not self.peeked:
			self.peeked=self.hackedreadline(size)
			self.readline=self.p_readline #now that we've peeked, we must use the peek-aware readline
		return self.peeked
	def p_readline(self, size=-1):
		r=self.peeked
		self.peeked=''
		self.readline=self.fobj.readline #ok, we can go back to the real readline
		if '\n' in r:
			return r
		return r+self.hackedreadline(size)

def doopen(filename,mode='r'):
	if filename=='-':
		fileobj=(mode[0]=='r' and sys.stdin or sys.stdout)
	if config.gzip>=2 or (config.gzip>=0 and string.lower(filename[-3:])=='.gz'):
		import gzip
		if string.find(mode,'b')<0:
			mode=mode+'b' #open gzip files in binary mode
		if filename=='-':
			if mode[0]=='r':
				import StringIO
				return PeekFile(gzip.GzipFile(mode=mode,fileobj=StringIO.StringIO(fileobj.read()))) #lovely hack since gzip.py requires a bunch of seeking.. bleh.
			return PeekFile(gzip.GzipFile(mode=mode,fileobj=fileobj))
		return PeekFile(gzip.open(filename,mode))
	else:
		if filename=='-':
			#return fileobj
			#return os.fdopen(fileobj.fileno(),mode) #eek, even worse.
			return PeekFile(fileobj,doclose=0)
		return PeekFile(open(filename,mode))

def testchecksumfile(type,file,filename,testfiles):
	line=1
	try:
		if not file:
			file=doopen(filename,'r')
		while 1:
			l=file.readline()
			if not len(l):
				return
			if type.dochecksumfileline(l,testfiles):
				stats.cferror=stats.cferror+1
				perror('%s : unrecognized line %i (CF)'%(perhaps_showpath(filename),line))
			line=line+1
	except EnvironmentError, a:
		stats.cferror=stats.cferror+1
		perror('%s : %s (CF)'%(perhaps_showpath(filename),a[1]))

#---------- md5 ----------


def testfilemd5(filename,filecrc):
	c=getfilemd5(filename)[0]
	if c!=string.lower(filecrc):
		return c

md5rem=re.compile(r'([0-9a-fA-F]{32}) ([ *])(.+)$')
def testmd5line(l,testfiles):
	x=md5rem.match(chomp(l))
	if not x: return -1
	if x.group(2)==' ':
		perror('warning: file %s tested in textmode' %x.group(3))
	testfile(testfiles,testfilemd5,x.group(3),x.group(1))

types['md5']=fv_type(
	testmd5line,
	(r'[0-9a-fA-F]{32} [ *].+',0),
	lambda o: string.find(o,'md5')>=0,
	lambda o: o+'.md5',
	lambda file: None,
	lambda a: '%s *%s\n'%(getfilemd5(a)[0],a)
)


#---------- sfv ----------

def testfilecrc32(filename,filecrc):
	c=getfilecrc(filename)[0]
	if string.atol(c,16)!=string.atol(filecrc,16):
		return c
		

sfvrem=re.compile(r'(.+) ([0-9a-fA-F]+)$')
def testsfvline(l,testfiles):
	if l[0]==';': return
	x=sfvrem.match(chomp(l))
	if not x: return -1
	testfile(testfiles,testfilecrc32,x.group(1),x.group(2))

types['sfv']=fv_type(
	testsfvline,
	#(r';.*generated (by|using) (.* on|.*sfv|.*crc32)',re.IGNORECASE),
	(r';|.+ [0-9a-fA-F]{8}',0),
	lambda o: o[-3:]=='sfv',
	lambda o: o+'.sfv',
	lambda file: file.write('; Generated by cfv v%s on %s\n;\n'%(version,time.strftime('%Y-%m-%d at %H:%M.%S',time.gmtime(time.time())))),
	lambda a: '%s %s\n'%(a,getfilecrc(a)[0])
)


#---------- csv ----------

csvrem=re.compile(r'([^,]+),([0-9]+),([0-9a-fA-F]+),')
def testcsvline(l,testfiles):
	x=csvrem.match(l)
	if not x: return -1
	testfile(testfiles,testfilecrc32,x.group(1),x.group(3),x.group(2))

def make_dofile_csv(a):
	c,s=getfilecrc(a)
	return '%s,%i,%s,\n'%(a,s,c)

types['csv']=fv_type(
	testcsvline,
	('[^,]+,[0-9]+,[0-9a-fA-F]+,[\n\r]*$',0),
	lambda o: o[-3:]=='csv',
	lambda o: o+'.csv',
	lambda file: None,
	make_dofile_csv
)


#---------- csv with 4 fields ----------

csv4rem=re.compile(r'([^,]+),([0-9]+),([0-9a-fA-F]+),([^,]*),')
def testcsv4line(l,testfiles):
	x=csv4rem.match(l)
	if not x: return -1
	testfile(testfiles,testfilecrc32,os.path.join(x.group(4),x.group(1)),x.group(3),x.group(2)) 

def make_dofile_csv4(a):
	c,s=getfilecrc(a)
	p=os.path.split(a)
	return '%s,%i,%s,%s,\n'%(p[1],s,c,p[0])

types['csv4']=fv_type(
	testcsv4line,
	(r'[^,]+,[0-9]+,[0-9a-fA-F]+,[^,]*,',0),
	lambda o: 0,
	lambda o: o+'.csv',
	lambda file: None,
	make_dofile_csv4
)


#---------- csv with only 2 fields ----------

csv2rem=re.compile(r'([^,]+),([0-9]+),')
def testcsv2line(l,testfiles):
	x=csv2rem.match(l)
	if not x: return -1
	testfile(testfiles,None,x.group(1),None,x.group(2))

def make_dofile_csv2(a):
	if a=='':
		s=getfilecrc(a)[1]#no way to get size of stdin other than to read it
	else:
		s=os.path.getsize(a)
	return '%s,%i,\n'%(a,s)
		
types['csv2']=fv_type(
	testcsv2line,
	('[^,]+,[0-9]+,[\n\r]*$',0),
	lambda o: 0,
	lambda o: o+'.csv',
	lambda file: None,
	make_dofile_csv2
)


#---------- generic ----------

def perhaps_showpath(file,dir=None):
	if config.showpaths==1 or (config.showpaths==2 and config.recursive):
		if dir==None:
			dir=curdir
		return os.path.join(dir,file)
	return file

ncdc={}
def nocase_dirfiles(dir,match):
	"return list of filenames in dir whose string.lower() equals match"
	dir=os.path.normpath(os.path.join(curdir,dir))
	if not ncdc.has_key(dir):
		d={}
		ncdc[dir]=d
		for a in os.listdir(dir):
			l=string.lower(a)
			if d.has_key(l):
				d[l].append(a)
			else:
				d[l]=[a]
	else:
		d=ncdc[dir]
	if d.has_key(match):
		return d[match]
	return []

def path_split(filename):
	"returns a list of components of filename"
	head=filename
	parts=[]
	while 1:
		head,tail=os.path.split(head)
		if len(tail):
			parts.insert(0,string.lower(tail))
		else:
			if len(head):
				parts.insert(0,string.lower(head))
			break
	return parts

def nocase_findfile(filename):
	cur="."
	parts=path_split(filename)
	#print 'nocase_findfile:',filename,parts,len(parts)
	for i in range(0,len(parts)):
		p=parts[i]
		#matches=filter(lambda f,p=p: string.lower(f)==p,dircache.listdir(cur)) #too slooow, even with dircache (though not as slow as without it ;)
		matches=nocase_dirfiles(cur,p) #nice and speedy :)
		#print 'i:',i,' cur:',cur,' p:',p,' matches:',matches
		if i==len(parts)-1:#if we are on the last part of the path, we want to match a file
			matches=filter(lambda f,cur=cur: os.path.isfile(os.path.join(cur,f)), matches)
		else:#otherwise, we want to match a dir
			matches=filter(lambda f,cur=cur: os.path.isdir(os.path.join(cur,f)), matches)
		if len(matches)==0:
			raise IOError, (errno.ENOENT,os.strerror(errno.ENOENT))
		if len(matches)>1:
			raise IOError, (errno.EEXIST,"More than one name matches %s"%os.path.join(cur,p))
		if cur=='.':
			cur=matches[0] #don't put the ./ on the front of the name
		else:
			cur=os.path.join(cur,matches[0])
	return cur
	
def fixpath(filename):
	return re.sub('['+re.escape(config.fixpaths)+']',os.sep,filename)

def normpath(filename):
	t=os.path.normpath(filename)
	if re.match(r"[a-z]:\\",t[0:3],re.I): #we can't use os.path.splitdrive, since we want to get rid of it even if we are not on a dos system.
		return t[3:]
	if t[0]==os.sep:
		return t[1:]
	return t

def testfile(testfiles,crctestfunc,filename,filecrc,filesize=-1):
	if len(config.fixpaths):
		filename=fixpath(filename)
	filename=normpath(filename)
	if testfiles:
		if config.ignorecase:
			if string.lower(filename) not in testfiles:
				return
		elif filename not in testfiles:
			return
	stats.num=stats.num+1
	l_filename=filename
	try:
		if config.ignorecase:
			#we need to find the correct filename if using showunverified, even if the filename we are given works, since on FAT/etc filesystems the incorrect case will still return true from os.path.exists, but it could be the incorrect case to remove from the unverified files list.
			if config.showunverified or not os.path.exists(l_filename):
				l_filename=nocase_findfile(l_filename)
		if config.showunverified:
			try:
				stats.unverifiedfiles[-1].remove(l_filename)
			except ValueError:
				pass #don't error if file has already been removed.
		if filesize>=0:
			fs=os.path.getsize(l_filename)
			if fs!=string.atoi(filesize):
				stats.badsize=stats.badsize+1
				raise CFVVerifyError, 'file size does not match (%s!=%i)'%(filesize,fs)
		if config.docrcchecks and filecrc!=None:
			c=crctestfunc(l_filename,filecrc)
			if c:
				stats.badcrc=stats.badcrc+1
				raise CFVVerifyError, 'crc does not match (%s!=%s)'%(filecrc,c)
		else:
			if not os.path.exists(l_filename):
				raise EnvironmentError, (errno.ENOENT,"missing")
			if not os.path.isfile(l_filename):
				raise EnvironmentError, (errno.ENOENT,"not a file")
			filecrc="exists" #since we didn't actually test the crc, make verbose mode merely say it exists
	except EnvironmentError, a:
		if a[0]==errno.ENOENT:
			stats.notfound=stats.notfound+1
		else:
			stats.ferror=stats.ferror+1
		perror('%s : %s'%(perhaps_showpath(l_filename),a[1]))
		return -1
	except CFVVerifyError, a:
		reninfo=''
		if config.rename:
			formatmap=make_rename_formatmap(l_filename) 
			for count in xrange(0,sys.maxint):
				formatmap['count']=count
				newfilename=config.renameformat%formatmap
				if config.renameformatnocount and count>0:
					newfilename='%s-%i'%(newfilename,count)
				if l_filename==newfilename:
					continue #if the filenames are the same they would cmp the same and be deleted. (ex. when renameformat="%(fullname)s")
				if os.path.exists(newfilename):
					if fcmp(l_filename, newfilename):
						os.unlink(l_filename)
						reninfo=' (dupe of %s removed)'%newfilename
						break
				else:
					os.rename(l_filename, newfilename)
					reninfo=' (renamed to %s)'%newfilename
					break
		perror('%s : %s%s'%(perhaps_showpath(l_filename),a,reninfo))
		return -2
	stats.ok=stats.ok+1
	if filesize>=0:
		pverbose('%s : OK (%i,%s)'%(perhaps_showpath(filename),fs,filecrc))
	else:
		pverbose('%s : OK (%s)'%(perhaps_showpath(filename),filecrc))

def test(filename,testfiles):
	try:
		file=doopen(filename,'r')
		l=file.peekline(512)#limit max line length to be read for testing, in case its a binary file or something.
	except EnvironmentError, a:
		stats.cferror=stats.cferror+1
		perror('%s : %s (CF)'%(perhaps_showpath(filename),a[1]))
		return -1
	for typename,type in types.items():
		if re.match(type.testmatch[0],l,type.testmatch[1]):
			if config.showunverified and filename in stats.unverifiedfiles[-1]:#we can't expect the checksum file itself to be checksummed
				stats.unverifiedfiles[-1].remove(filename)
			pverbose('testing from %s (%s)'%(filename,typename))
			testchecksumfile(type,file,filename,testfiles)
			return
	perror("I don't recognize the type of %s"%filename)
	stats.cferror=stats.cferror+1

def make(type,ifilename,testfiles):
	file=None
	if ifilename:
		filename=ifilename
	else:
		filename=type.make_std_filename(os.path.basename(curdir))
		if config.gzip==1 and filename[-3:]!='.gz': #if user does -zz, perhaps they want to force the filename to be kept?
			filename=filename+'.gz'
	if os.path.exists(filename):
		perror("%s already exists"%filename)
		sys.exit(1)
	if not testfiles or not len(testfiles):
		tfauto=1
		testfiles=os.listdir('.')
		if config.dirsort:
			testfiles.sort()
	else:
		tfauto=0
	testdirs=[]
	
	i=0
	while i<len(testfiles):
		f=testfiles[i]
		i=i+1
		if not tfauto and f=='-':
			f=''
		elif not os.path.isfile(f):
			if config.recursive and os.path.isdir(f):
				if config.recursive==1:
					testdirs.append(f)
				elif config.recursive==2:
					rfiles=os.listdir(f)
					if config.dirsort:
						rfiles.sort()
					testfiles[i:i]=map(lambda x,p=f: os.path.join(p,x), rfiles)
				continue
			if tfauto:#if user isn't specifying files, don't even try to add dirs and stuff, and don't print errors about it.
				continue
		stats.num=stats.num+1
		if file==None:
			file=doopen(filename,'w')
			type.writeheader(file)
			dof=type.make_dofile
		try:
			s=dof(f)
		except EnvironmentError, a:
			if a[0]==errno.ENOENT:
				stats.notfound=stats.notfound+1
			else:
				stats.ferror=stats.ferror+1
			perror('%s : %s'%(f,a[1]))
			continue
		file.write(s)
		stats.ok=stats.ok+1
	file.close()
	
	for f in testdirs:
		olddir=curdir
		chdir(f)
		make(type,ifilename,None)
		chdir(olddir)

def start_test_dir(args):
	if not config.showunverified:
		return
	if len(args):
		filelist=args[:] #make a copy so we don't modify the args list (teehee)
	else:
		filelist=os.listdir('.')
	#dir=curdir
	#stats.unverifiedfiles[dir]=map(lambda d: os.path.join(dir,d),filelist))
	#stats.unverifiedfiles[dir]=filelist
	stats.unverifiedfiles.append(filelist)
def finish_test_dir():
	if not config.showunverified:
		return
	unverified=stats.unverifiedfiles.pop()
	for file in unverified:
		if os.path.isfile(file):
			perror('%s : not verified'%perhaps_showpath(file,curdir))
			stats.unverified=stats.unverified+1

atrem=re.compile(r'md5|\.(csv|sfv)(\.gz)?$',re.IGNORECASE)#md5sum files have no standard extension, so just search for files with md5 in the name anywhere, and let the test func see if it really is one.
def autotest(args):
	start_test_dir(args)
	for a in os.listdir('.'):
		if config.recursive and os.path.isdir(a):
			olddir=curdir
			chdir(a)
			autotest(args)
			chdir(olddir)
		if atrem.search(a):
			test(a,args)
	finish_test_dir()

def printusage(err=0):
	perror('Usage: cfv [opts] [-p dir] [-T|-C] [-t type] [-f file] [files...]')
	perror('  -r       recursive mode 1 (make seperate chksum files for each dir)')
	perror('  -rr      recursive mode 2 (make a single file with deep listing in it)')
	perror('  -R       not recursive (default)')
	perror('  -T       test mode (default)')
	perror('  -C       create mode')
	perror('  -t <t>   set type to <t> (%s, or auto(default))'%string.join(types.keys(),', ')) #reduce(lambda a,b: a+b+', ',types.keys(),''))
	perror('  -f <f>   use <f> as list file')
	perror('  -m       check only for missing files (don\'t compare checksums)')
	perror('  -M       check checksums (default)')
	perror('  -n       rename bad files')
	perror('  -N       don\'t rename bad files (default)')
	perror('  -p <d>   change to directory <d> before doing anything')
	perror('  -i       ignore case')
	perror('  -I       don\'t ignore case (default)')
	perror('  -u       show unverified files')
	perror('  -U       don\'t show unverified files (default)')
	perror('  -v       verbose')
	perror('  -V       not verbose (default)')
	perror('  -VV      don\'t print status line at end either')
	perror('  -q       quiet mode.  check exit code for success.')
	perror('  -zz      force making gzipped files, even if not ending in .gz')
	perror('  -z       make gzipped files in auto create mode')
	perror('  -Z       don\'t create gzipped files automatically. (default)')
	perror('  -ZZ      never use gzip, even if file ends in .gz')
	perror(' --fixpaths=<s>  replace any chars in <s> with %s'%os.sep)
	perror(' --showpaths=<p> show full paths 0:never, 1:always, 2:in recursive mode(default)')
	perror(' --renameformat=<f> format string to use with -n option')
	perror(' --help/-h show help')
	perror(' --version show cfv and module versions')
	sys.exit(err)
def printhelp():
	perror('cfv v%s - Copyright (C) 2000-2001 Matthew Mueller - GPL license'%version)
	printusage()

stats=Stats()
config=Config()


def main(argv):
	manual=0
	mode=0
	type='auto'
	starttime=time.time()

	try:
		optlist, args = getopt.getopt(argv, 'rRTCt:f:mMnNp:uUiIvVzZqh?', ['fixpaths=','showpaths=','renameformat=','help','version'])
	except getopt.error, a:
		perror("cfv: %s"%a)
		printusage(1)

	if config.cmdlinesort:
		args.sort()

	prevopt=''
	for o,a in optlist:
		if o=='-T':
			mode=0
		elif o=='-C':
			mode=1
		elif o=='-t':
			if not a in ['auto']+types.keys():
				perror('type %s not recognized'%a)
				sys.exit(1)
			type=a
		elif o=='-f':
			manual=1 #filename selected manually, don't try to autodetect
			filename=a
			if mode==0:
				if config.ignorecase:
					args=map(string.lower,args) #lowercase it all now, so we don't have to keep doing it over and over in the testfile
				start_test_dir(args)
				if type=='auto':
					test(a,args)
				else:
					testchecksumfile(types[type],None,filename,args)
					#types[type].checksumfile(open(a,'r'),args)
				finish_test_dir()
			else:
				if type!='auto':
					make(types[type],a,args)
				else:
					ok=0
					testa=''
					if config.gzip>=0 and a[-3:]=='.gz':
							testa=a[:-3]
					for type in types.values():
						if type.automake_filenametest(a) or (testa and type.automake_filenametest(testa)):
							make(type,a,args)
							ok=1
							break;
					if not ok:
						perror('specify a filetype with -t, or use standard extension')
						sys.exit(1)
		elif o=='-U':
			config.showunverified=0
		elif o=='-u':
			config.showunverified=1
		elif o=='-I':
			config.ignorecase=0
		elif o=='-i':
			config.ignorecase=1
		elif o=='-n':
			config.rename=1
		elif o=='-N':
			config.rename=0
		elif o=='--renameformat':
			config.setx('renameformat', a)
		elif o=='-m':
			config.docrcchecks=0
		elif o=='-M':
			config.docrcchecks=1
		elif o=='-p':
			chdir(a)
		elif o=='-r':
			if prevopt=='-r':
				config.recursive=2
			else:
				config.recursive=1
		elif o=='-R':
			config.recursive=0
		elif o=='-v':
			config.verbose=1
		elif o=='-V':
			if prevopt=='-V':
				config.verbose=-1
			else:
				config.verbose=0
		elif o=='-q':
			config.verbose=-2
		elif o=='-z':
			if prevopt=='-z':
				config.gzip=2
			else:
				config.gzip=1
		elif o=='-Z':
			if prevopt=='-Z':
				config.gzip=-1
			else:
				config.gzip=0
		elif o=='--showpaths':
			config.showpaths=int(a)
		elif o=='--fixpaths':
			config.fixpaths=a
		elif o=='-h' or o=='-?' or o=='--help':
			printhelp()
		elif o=='--version':
			print 'cfv %s'%version
			try: print 'fchksum %s'%fchksum.version()
			except NameError: pass
			print 'python %08x-%s'%(sys.hexversion,sys.platform)
			sys.exit(0)
		prevopt=o

	if not manual:
		if mode==0:
			if config.ignorecase:
				args=map(string.lower,args) #lowercase it all now, so we don't have to keep doing it over and over in the testfile
			autotest(args)
		else:
			if type=='auto':
				type=config.defaulttype
			make(types[type],None,args)

	pinfo('%i files'%stats.num,0)
	pinfo(', %i OK' %stats.ok,0)
	if stats.badcrc:
		pinfo(', %i badcrc' %stats.badcrc,0)
	if stats.badsize:
		pinfo(', %i badsize' %stats.badsize,0)
	if stats.notfound:
		pinfo(', %i not found' %stats.notfound,0)
	if stats.ferror:
		pinfo(', %i file errors' %stats.ferror,0)
	if stats.unverified:
		pinfo(', %i unverified' %stats.unverified,0)
	if stats.cferror:
		pinfo(', %i chksum file errors' %stats.cferror,0)

	elapsed=time.time()-starttime
	pinfo('.  %.3f seconds, '%(elapsed),0)
	if elapsed==0.0:
		pinfo('%.1fK'%(stats.bytesread/1024.0),0)
	else:
		pinfo('%.1fK/s'%(stats.bytesread/elapsed/1024.0),0)

	pinfo('\n',0)

#if stats.badcrc or stats.badsize or stats.notfound or stats.ferror or stats.unverified or stats.cferror:
#	sys.exit(1)
	sys.exit((stats.badcrc and 2) | (stats.badsize and 4) | (stats.notfound and 8) | (stats.ferror and 16) | (stats.unverified and 32) | (stats.cferror and 64))

if __name__=='__main__':
	main(sys.argv[1:])
