#!/usr/local/bin/pypy
#	JiYuSoft Ltd., All rights reserved.
#	All use is free. No warranty!
#	Tiny grep replacement

from __future__ import print_function
from sys import argv, exit
from os.path import islink, isdir, isfile, basename
from os import listdir
from re import match, compile as recomp, IGNORECASE as IC, MULTILINE as ML, DOTALL as DA
from sys import stderr

bnm = basename (argv[0])
def myend (*a):
	print (bnm, *a, sep=': ', file=stderr)
	exit(1)

opt = {
'r': False, # recursive
'l': False, # list files
'i': False, # case-insensitive
'v': False, # invert match logic: non-matching lines (or with y, files)
'c': False, # count number of matching lines (or with y, matches)
'e': False, # dummy option to allow things like: mgrp -e -g Makefile, or, grep --help | mgrp -e -r
'm': False, # max number of matches per file
'M': False, # max number of matching files
'y': False, # By default matching is line-based. This allows multi-line match: no line boundaries (^$ will still match line start/end though)

# All input is eligible by default. The following two options' names should not confuse you.
# All of the below is for excluding some of the inputs.
# An input file's name must match against inc pattern (if given), AND
# its name must not match against exc pattern (if given)
'n': False, # include file pattern (case-insensitive)
'N': False, # include file pattern
'x': False, # exclude file pattern (case-insensitive)
'X': False, # exclude file pattern
'd': False, # exclude directory pattern (case-insensitive) # exclude directories matching this
'D': False, # exclude directory pattern
}

def usage (*a):
	print ('Usage:', bnm, '[-'+ ''.join (sorted (opt.keys()))+ ']', '[inc/exc patterns & max # of matches/files] pattern [files/dirs]')
	print ("\t'inc/exc patterns & max # of matches/files' will be parsed in the order they are given in options")
	myend (*a)

if len(argv) < 2:
	usage ('Not enough arguments')

maxm = 0 # max number of matches per file
MaxM = 0 # max number of matching files
nxl = []
dre = None
o = argv[1] # options only at first arg
istrt = 1	# input start

if match (r'-[-\w]+$', o):
	minlen = 3
	nxpat = False
	patm = False
	for i in xrange (1, len(o)):
		c = o[i]
		try:
			if opt[c]:
				usage ('Repeating option', c)
		except KeyError:
			usage ('Unrecognized option', c)
		opt[c] = True

		if c in 'mMnxdNXD':
			if c not in 'mM':
				if opt[c.swapcase()]:
					usage ('Repeating option', c)
				nxpat = True
			minlen += 1 # each will require an input in the 'inc/exc patterns & max # of matches/files' list
			patm = True

	if opt['r'] or opt['l'] or nxpat: # if recursive or list, or any inc/exc pattern: these require a file/dir input
		minlen += 1
	if len(argv) < minlen:
		usage ('Not enough arguments')

	if opt['y'] and opt['v'] and (not opt['l'] or opt['c']):
		myend ('Options y & v can be used together if', 'l is, and c is not, used')

	istrt += 1 # done with error-checking options
	if patm:
		for i in xrange (1, len(o)): # retrieve & compile inputs in the 'inc/exc patterns & max # of matches/files' list
			c = o[i]
			if c == 'm':
				try:
					maxm = int (argv[istrt])
				except ValueError:
					myend ('Invalid value for option m', argv[istrt])
				if maxm <= 0:
					myend ('Invalid value for option m', maxm)
				istrt += 1
			elif c == 'M':
				try:
					MaxM = int (argv[istrt])
				except ValueError:
					myend ('Invalid value for option M', argv[istrt])
				if MaxM <= 0:
					myend ('Invalid value for option M', MaxM)
				istrt += 1
			elif c == 'd':
				dre = recomp (argv[istrt], DA | IC)
				istrt += 1
			elif c == 'D':
				dre = recomp (argv[istrt], DA)
				istrt += 1
			elif c == 'n':
				nxl.append ( (True, recomp (argv[istrt], DA | IC)) )
				istrt += 1
			elif c == 'N':
				nxl.append ( (True, recomp (argv[istrt], DA)) )
				istrt += 1
			elif c == 'x':
				nxl.append ( (False, recomp (argv[istrt], DA | IC)) )
				istrt += 1
			elif c == 'X':
				nxl.append ( (False, recomp (argv[istrt], DA)) )
				istrt += 1

i = ML
if opt['y']:
	i = i | DA

if opt['i']:
	re1 = recomp (argv[istrt], i | IC) # pattern
else:
	re1 = recomp (argv[istrt], i)
istrt += 1

nic = len(argv) - istrt # number of input files/folders on the commandline

if nic > 1 or nic == 1 and isdir (argv[istrt]):
	prnm = True # print name
else:
	prnm = False

def nxfile(f):
	for m in nxl:
		c = m[1].search(f)
		if m[0] and not c or not m[0] and c:
			return False # skip this file
	return True

def proc(f): # process a single file
	c = 0
	try:
		with open(f, 'r') as F:
			for a in F:
				m = re1.search(a)
				if opt['v']:
					m = not m
				if m:
					if not opt['c']:
						if opt['l']:
							print(f) # not counting and listing, return on 1st match
							return 1

						if prnm: # print name?
							print (f, ':', sep='', end='')
						print (a, end='') # not counting and not listing: print matching line

					c += 1
					if 0 < maxm <= c:
						break
	except IOError, e:
		print (bnm, e, sep=': ', file=stderr)

	if opt['c'] and c > 0:
		if opt['l'] or prnm:
			print (f, ':', sep='', end='')
		print(c)
	return c

def procy(f): # process a single file, multi-line match
	try:
		with open(f, 'r') as F:
			b = F.read()
	except IOError, e:
		print (bnm, e, sep=': ', file=stderr)
		return 0

	c = 0
	for m in re1.finditer(b):
		if opt['v']:
			return 0

		if not opt['c']:
			if opt['l']:
				print(f) # not counting and listing, return on 1st match
				return 1

			if prnm: # print name?
				print (f, ':', sep='', end='')
			print (m.group(0)) # not counting and not listing: print match

		c += 1
		if 0 < maxm <= c:
			break

	if opt['v']: # y & v, then l (and no c)
		print(f)
		return 1

	if opt['c'] and c > 0:
		if opt['l'] or prnm:
			print (f, ':', sep='', end='')
		print(c)
	return c

if opt['y']:
	proc = procy

tnf = 0 # total number of matching files
class TooMany (Exception):
	pass

def mygrepr(d): # recursive, d is a dir
	global tnf
	for e in listdir(d):
		g = d+ '/'+ e
		if not islink(g):
			if isfile(g):
				if nxfile(e) and proc(g) > 0:
					tnf += 1
					if 0 < MaxM <= tnf:
						raise TooMany
			elif isdir(g) and not (dre and dre.search(e)):
				mygrepr(g)

try:
	if nic == 0:
		proc ('/dev/stdin') # if no input given, use stdin
	else:
		for i in xrange (istrt, len(argv)):
			g = argv[i]
			if not islink(g):
				e = basename(g)
				if isfile(g):
					if nxfile(e) and proc(g) > 0:
						tnf += 1
						if 0 < MaxM <= tnf:
							raise TooMany
				elif opt['r'] and isdir(g) and not (dre and dre.search(e)):
					mygrepr(g)
except (TooMany, KeyboardInterrupt):
	pass
