#!/usr/bin/env python # encoding: utf-8 # Baptiste Lepilleur, 2009 from dircache import listdir import re import fnmatch import os.path # These fnmatch expressions are used by default to prune the directory tree # while doing the recursive traversal in the glob_impl method of glob function. prune_dirs = '.git .bzr .hg .svn _MTN _darcs CVS SCCS ' # These fnmatch expressions are used by default to exclude files and dirs # while doing the recursive traversal in the glob_impl method of glob function. ##exclude_pats = prune_pats + '*~ #*# .#* %*% ._* .gitignore .cvsignore vssver.scc .DS_Store'.split() # These ant_glob expressions are used by default to exclude files and dirs and also prune the directory tree # while doing the recursive traversal in the glob_impl method of glob function. default_excludes = ''' **/*~ **/#*# **/.#* **/%*% **/._* **/CVS **/CVS/** **/.cvsignore **/SCCS **/SCCS/** **/vssver.scc **/.svn **/.svn/** **/.git **/.git/** **/.gitignore **/.bzr **/.bzr/** **/.hg **/.hg/** **/_MTN **/_MTN/** **/_darcs **/_darcs/** **/.DS_Store ''' DIR = 1 FILE = 2 DIR_LINK = 4 FILE_LINK = 8 LINKS = DIR_LINK | FILE_LINK ALL_NO_LINK = DIR | FILE ALL = DIR | FILE | LINKS _ANT_RE = re.compile( r'(/\*\*/)|(\*\*/)|(/\*\*)|(\*)|(/)|([^\*/]*)' ) def ant_pattern_to_re( ant_pattern ): """Generates a regular expression from the ant pattern. Matching convention: **/a: match 'a', 'dir/a', 'dir1/dir2/a' a/**/b: match 'a/b', 'a/c/b', 'a/d/c/b' *.py: match 'script.py' but not 'a/script.py' """ rex = ['^'] next_pos = 0 sep_rex = r'(?:/|%s)' % re.escape( os.path.sep ) ## print 'Converting', ant_pattern for match in _ANT_RE.finditer( ant_pattern ): ## print 'Matched', match.group() ## print match.start(0), next_pos if match.start(0) != next_pos: raise ValueError( "Invalid ant pattern" ) if match.group(1): # /**/ rex.append( sep_rex + '(?:.*%s)?' % sep_rex ) elif match.group(2): # **/ rex.append( '(?:.*%s)?' % sep_rex ) elif match.group(3): # /** rex.append( sep_rex + '.*' ) elif match.group(4): # * rex.append( '[^/%s]*' % re.escape(os.path.sep) ) elif match.group(5): # / rex.append( sep_rex ) else: # somepath rex.append( re.escape(match.group(6)) ) next_pos = match.end() rex.append('$') return re.compile( ''.join( rex ) ) def _as_list( l ): if isinstance(l, basestring): return l.split() return l def glob(dir_path, includes = '**/*', excludes = default_excludes, entry_type = FILE, prune_dirs = prune_dirs, max_depth = 25): include_filter = [ant_pattern_to_re(p) for p in _as_list(includes)] exclude_filter = [ant_pattern_to_re(p) for p in _as_list(excludes)] prune_dirs = [p.replace('/',os.path.sep) for p in _as_list(prune_dirs)] dir_path = dir_path.replace('/',os.path.sep) entry_type_filter = entry_type def is_pruned_dir( dir_name ): for pattern in prune_dirs: if fnmatch.fnmatch( dir_name, pattern ): return True return False def apply_filter( full_path, filter_rexs ): """Return True if at least one of the filter regular expression match full_path.""" for rex in filter_rexs: if rex.match( full_path ): return True return False def glob_impl( root_dir_path ): child_dirs = [root_dir_path] while child_dirs: dir_path = child_dirs.pop() for entry in listdir( dir_path ): full_path = os.path.join( dir_path, entry ) ## print 'Testing:', full_path, is_dir = os.path.isdir( full_path ) if is_dir and not is_pruned_dir( entry ): # explore child directory ? ## print '===> marked for recursion', child_dirs.append( full_path ) included = apply_filter( full_path, include_filter ) rejected = apply_filter( full_path, exclude_filter ) if not included or rejected: # do not include entry ? ## print '=> not included or rejected' continue link = os.path.islink( full_path ) is_file = os.path.isfile( full_path ) if not is_file and not is_dir: ## print '=> unknown entry type' continue if link: entry_type = is_file and FILE_LINK or DIR_LINK else: entry_type = is_file and FILE or DIR ## print '=> type: %d' % entry_type, if (entry_type & entry_type_filter) != 0: ## print ' => KEEP' yield os.path.join( dir_path, entry ) ## else: ## print ' => TYPE REJECTED' return list( glob_impl( dir_path ) ) if __name__ == "__main__": import unittest class AntPatternToRETest(unittest.TestCase): ## def test_conversion( self ): ## self.assertEqual( '^somepath$', ant_pattern_to_re( 'somepath' ).pattern ) def test_matching( self ): test_cases = [ ( 'path', ['path'], ['somepath', 'pathsuffix', '/path', '/path'] ), ( '*.py', ['source.py', 'source.ext.py', '.py'], ['path/source.py', '/.py', 'dir.py/z', 'z.pyc', 'z.c'] ), ( '**/path', ['path', '/path', '/a/path', 'c:/a/path', '/a/b/path', '//a/path', '/a/path/b/path'], ['path/', 'a/path/b', 'dir.py/z', 'somepath', 'pathsuffix', 'a/somepath'] ), ( 'path/**', ['path/a', 'path/path/a', 'path//'], ['path', 'somepath/a', 'a/path', 'a/path/a', 'pathsuffix/a'] ), ( '/**/path', ['/path', '/a/path', '/a/b/path/path', '/path/path'], ['path', 'path/', 'a/path', '/pathsuffix', '/somepath'] ), ( 'a/b', ['a/b'], ['somea/b', 'a/bsuffix', 'a/b/c'] ), ( '**/*.py', ['script.py', 'src/script.py', 'a/b/script.py', '/a/b/script.py'], ['script.pyc', 'script.pyo', 'a.py/b'] ), ( 'src/**/*.py', ['src/a.py', 'src/dir/a.py'], ['a/src/a.py', '/src/a.py'] ), ] for ant_pattern, accepted_matches, rejected_matches in list(test_cases): def local_path( paths ): return [ p.replace('/',os.path.sep) for p in paths ] test_cases.append( (ant_pattern, local_path(accepted_matches), local_path( rejected_matches )) ) for ant_pattern, accepted_matches, rejected_matches in test_cases: rex = ant_pattern_to_re( ant_pattern ) print 'ant_pattern:', ant_pattern, ' => ', rex.pattern for accepted_match in accepted_matches: print 'Accepted?:', accepted_match self.assert_( rex.match( accepted_match ) is not None ) for rejected_match in rejected_matches: print 'Rejected?:', rejected_match self.assert_( rex.match( rejected_match ) is None ) unittest.main()