Update the test suite to match the new signature of the cache methods
Patch by: Sverre Rabbelier
#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# __doc__ string is slightly unconventional because it is used as usage text
"""%prog [OPTIONS] [FIND_REGEX] [REPLACE_FORMAT]
Script to list, search, and modify files using Python regex patterns.
OPTIONS: optional command-line flags; see %prog --help
FIND_REGEX: an optional valid Python regular expression pattern;
if supplied, only files containing at least one match will be processed;
matching file paths will be printed; if supplied, REPLACE_FORMAT will be
used to convert the match groups into formatted output.
REPLACE_FORMAT: an optional valid Python format string;
FIND_REGEX must be supplied first if REPLACE_FORMAT is supplied;
positional arguments will be replaced with ordered groups from
FIND_REGEX matches, and named arguments will be replaced with named
groups from FIND_REGEX matches."""
__authors__ = [
'"Todd Larsen" <tlarsen@google.com>',
]
import dircache
import errno
import os
import optparse
import re
import sre_constants
import sys
class Error(Exception):
"""Base class of all exceptions in this module.
"""
pass
def compileRegex(pattern):
"""Compiles a Python regex pattern into a regex object.
Args:
pattern: valid Python regex pattern string, or an already-compiled
regex object (in which case this function is is a no-op)
Returns:
regex object compiled from pattern
Raises:
Error if pattern could not be compiled.
"""
try:
return re.compile(pattern)
except sre_constants.error, error:
msg = 're.compile: %s\n%s' % (error.args[0], pattern)
raise Error(errno.EINVAL, msg)
def findAll(text_to_search, pattern):
"""Returns all matches of a regex in a string.
Args:
text_to_search: string in which to find matches
pattern: Python regex pattern (or already-compiled regex object)
indicating which matches to retrieve
Returns:
a (possibly empty) list of the matches found, as strings
"""
matches = []
def _captureMatchText(match):
match_text = match.group()
matches.append(match_text)
return match_text
compileRegex(pattern).sub(_captureMatchText, text_to_search)
return matches
def getFileContents(file_path):
"""Reads the contents of a file as a single string, then closes the file.
Args:
file_path: path to the file to read its contents into a string
Returns:
a single string containing the entire contents of the file
"""
file_to_read = open(file_path)
file_contents = file_to_read.read()
file_to_read.close()
return file_contents
def findAllInFile(file_path, pattern, *ignored_args, **ignored_kwargs):
"""Action to return a list of all pattern matches in a file.
Args:
file_path: path of file to manipulate
pattern: see findAll()
*ignored_args: other positional arguments which are ignored
command-line arguments not used by this action callable
**ignored_kwargs: other keyword arguments which are ignored
command-line options not used by this action callable
Returns:
two-tuple of boolean indicating if any match was found and a
(possibly empty) list of the matches found, as strings (to be used
as printable output of the action)
"""
matches = findAll(getFileContents(file_path), pattern)
if matches:
found = True
else:
found = False
return found, matches
def replaceAll(original, pattern, format):
"""Substitutes formatted text for all matches in a string.
Args:
original: original string in which to find and replace matches
pattern: Python regex pattern (or already-compiled regex object)
indicating which matches to replace
format: Python format string specifying how to format the
replacement text; how this format string is interpreted depends
on the contents of the pattern; if the pattern contains:
named groups: format is expected to contain named format specifiers
unnamed groups: format is expected to contain exactly the same
number of unnamed format specifiers as the number of groups in
pattern
no groups: format is expected to contain a single format specifier
(in which case the entire match is supplied to it), or no format
specifier at all (in which case the "format" string simply
replaces the match with no substitutions from the match itself)
Returns:
two-tuple of the text with all matches replaced as specified by
pattern and format, and a list of the original matches, each followed
by its replacement
"""
matches_and_replacements = []
def _replaceWithFormat(match):
formatted_match = None
if match.groupdict():
try:
formatted_match = format % match.groupdict()
except TypeError:
pass
if (not formatted_match) and match.groups():
try:
formatted_match = format % match.groups()
except TypeError:
pass
if (not formatted_match):
try:
formatted_match = format % match.group()
except TypeError:
formatted_match = format
matches_and_replacements.append(match.group())
matches_and_replacements.append(formatted_match)
return formatted_match
replaced = compileRegex(pattern).sub(_replaceWithFormat, original)
return replaced, matches_and_replacements
def writeAltFileIfExt(path, ext, contents):
"""Writes a file if path and additional extension are supplied.
If path or ext are not supplied, no file is written.
Args:
path: path of file to be written, to which ext will be appended
ext: additional file extension that will be appended to path
contents: contents of file to be written, as a string
"""
if (not path) or (not ext):
return
if ext.startswith('.'):
ext = ext[1:]
alt_path = '%s.%s' % (path, ext)
alt_file = open(alt_path, 'w')
alt_file.write(contents)
alt_file.close()
def replaceAllInFile(file_path, pattern, format,
new_ext=None, backup_ext=None,
overwrite_files=False,
*ignored_args, **ignored_kwargs):
"""Substitutes formatted text for all matches in a file.
Args:
file_path: path of file to manipulate
pattern, format: see replaceAll()
*ignored_args: other positional arguments which are ignored
command-line arguments not used by this action callable
**ignored_kwargs: other keyword arguments which are ignored
command-line options not used by this action callable
Returns:
two-tuple of boolean indicating if any match was found and a
list of printable output text lines containing pairs of original
pattern matches each followed by the formatted replacement
"""
original = getFileContents(file_path)
replaced, matches_and_replacements = replaceAll(
original, pattern, format)
if matches_and_replacements:
found = True
writeAltFileIfExt(file_path, new_ext, replaced)
writeAltFileIfExt(file_path, backup_ext, original)
if overwrite_files:
if replaced != original:
replaced_file = open(file_path, 'w')
replaced_file.write(replaced)
replaced_file.close()
else:
found = False
return found, matches_and_replacements
def listFile(*ignored_args, **ignored_kwargs):
"""No-op action callable that ignores arguments and returns (True, []).
"""
return True, [] # match only based on file names, which was done by caller
def applyActionToFiles(action, action_args,
start_path='', abs_path=False, files_pattern='',
recurse_dirs=False, dirs_pattern='',
follow_symlinks=False, quiet_output=False,
hide_paths=False, hide_text=False, **action_options):
"""Applies a callable action to files, based on options and arguments.
Args:
action: callable that expects a file path argument, positional arguments
(action_args), and keyword options from the command-line options dict;
and returns a "matched" boolean and a list of output strings
action_args: list of positional arguments, if any; passed to action
callable unchanged
start_path: required path of initial directory to visit
abs_path: optional boolean indicating to use absolute paths
files_pattern: required Python regex (object or pattern) which selects
which files to pass to the action callable
recurse_dirs: boolean indicating if subdirectories should be traversed
dirs_pattern: Python regex (object or pattern) which selects which
subdirectories to traverse if recurse_dirs is True
follow_symlinks: boolean indicating if symlinks should be traversed
quiet_output: optional boolean indicating if output should be suppressed
hide_paths: optional boolean indicating to omit file paths from output
hide_text: optional boolean indicating to omit find/replace text from
output
**action_options: remaining keyword arguments that are passed unchanged
to the action callable
Returns:
two-tuple containing an exit code and a (possibly empty) list of
output strings
Raises:
Error exception if problems occur (file I/O, invalid regex, etc.).
"""
exit_code = errno.ENOENT
output = []
start_path = os.path.expandvars(os.path.expanduser(start_path))
if abs_path:
start_path = os.path.abspath(start_path)
paths = [start_path]
files_regex = compileRegex(files_pattern)
if recurse_dirs:
dirs_regex = compileRegex(dirs_pattern)
while paths:
sub_paths = []
for path in paths:
# expand iterator into an actual list and sort it
try:
items = dircache.listdir(path)[:]
except (IOError, OSError), error:
raise Error(error.args[0], '%s: %s' % (
error.__class__.__name__, error.args[1]))
items.sort()
for item in items:
item_path = os.path.join(path, item)
if os.path.islink(item_path):
if not follow_symlinks:
continue # do not follow symlinks (ignore them)
if os.path.isdir(item_path):
if recurse_dirs:
if dirs_regex.match(item):
sub_paths.append(item_path)
continue
if os.path.isfile(item_path) and files_regex.match(item):
try:
matched, found_output = action(item_path, *action_args,
**action_options)
except (IOError, OSError), error:
raise Error(error.args[0], '%s: %s' % (
error.__class__.__name__, error.args[1]))
if matched:
exit_code = 0 # at least one matched file has now been found
if (not quiet_output) and (not hide_paths):
output.append(item_path)
if (not quiet_output) and (not hide_text):
output.extend(found_output)
paths = sub_paths
return exit_code, output
class _ErrorOptionParser(optparse.OptionParser):
"""Customized optparse.OptionParser that does not call sys.exit().
"""
def error(self, msg):
"""Raises an Error exception, instead of calling sys.exit().
"""
raise Error(errno.EINVAL, msg)
def _buildParser():
"""Returns a custom OptionParser for parsing command-line arguments.
"""
parser = _ErrorOptionParser(__doc__)
filter_group = optparse.OptionGroup(parser,
'File Options',
'Options used to select which files to process.')
filter_group.add_option(
'-f', '--files', dest='files_pattern',
default='(?!^.*\.pyc|.*\.ico|.*\.gif|.*\.png|.*\.jpg$)',
metavar='FILES_REGEX',
help=('Python regex pattern (*not* a glob!) defining files to process'
' in each directory [default: %default]'))
filter_group.add_option(
'-F', '--follow', dest='follow_symlinks', default=False,
action='store_true',
help=('follow file and subdirectory symlinks (possibly *DANGEROUS*)'
' [default: %default]'))
parser.add_option_group(filter_group)
dir_group = optparse.OptionGroup(parser,
'Directory Options',
'Options used to indicate which directories to traverse.')
dir_group.add_option(
'-s', '--start', dest='start_path', default=os.curdir, metavar='PATH',
help='directory in which to start processing files [default: %default]')
dir_group.add_option(
'-R', '--recursive', dest='recurse_dirs', default=False,
action='store_true',
help='recurse into subdirectories [default: %default]')
dir_group.add_option(
'-d', '--dirs', dest='dirs_pattern', default='^[^.].*$',
metavar='SUBDIRS_REGEX',
help=('Python regex pattern (*not* a glob!) defining subdirectories to'
' recurse into (if --recursive) [default: %default]'))
parser.add_option_group(dir_group)
output_group = optparse.OptionGroup(parser,
'Output Options',
'Options used to control program output.')
output_group.add_option(
'-a', '--abspath', dest='abs_path', default=False, action='store_true',
help=('output absolute paths instead of relative paths'
' [default: %default]'))
output_group.add_option(
'', '--nopaths', dest='hide_paths', default=False, action='store_true',
help=('suppress printing of file path names for successfully matched'
' files to stdout [default: %default]'))
output_group.add_option(
'', '--notext', dest='hide_text', default=False, action='store_true',
help=('suppress find/replace text output to stdout (but still print'
' paths if not --nopath, and still perform replacements if'
' specified) [default: %default]'))
output_group.add_option(
'-q', '--quiet', dest='quiet_output', default=False, action='store_true',
help=('suppress *all* printed output to stdout (but still perform'
' replacements if specified) [default: %default]'))
parser.add_option_group(output_group)
replace_group = optparse.OptionGroup(parser,
'Replace Options',
'Options applied when matches in files are replaced with substitutions.'
' (Only possible if REPLACE_FORMAT is supplied.)')
replace_group.add_option(
'-o', '--overwrite', dest='overwrite_files', default=False,
action='store_true',
help=('overwrite original files with formatted text substituted for'
' matches [default: %default]'))
replace_group.add_option(
'-b', '--backup', dest='backup_ext', default='', metavar='EXTENSION',
help=('if supplied, and file would be overwritten, backup original'
' file with the supplied extension [default is no backups of'
' overwritten files are kept]'))
replace_group.add_option(
'-n', '--new', dest='new_ext', default='', metavar='EXTENSION',
help=('if supplied, and file has matches and and is altered by'
' substitutions, create a new file with the supplied extension'
' [default is no new file is created]'))
parser.add_option_group(replace_group)
return parser
def _parseArgs(cmd_line_args):
"""Builds a command-line option parser and parses command-line arguments.
Args:
cmd_line_args: command-line arguments, excluding the argv[0] program name
Returns:
four-tuple of action callable, supplied command-line options (including
those defined by defaults in the command-line parser) as a dict,
remaining positional command-line arguments, and the parser itself
Raises:
Error if problems occurred during commmand-line argument parsing.
"""
parser = _buildParser()
options, args = parser.parse_args(args=cmd_line_args)
if not args:
# no FIND_REGEX or REPLACE_PATTERN supplied, so just match based
# on file name and subdirectory name patterns
action = listFile
elif len(args) == 1:
# FIND_REGEX supplied, but not REPLACE_PATTERN, so just match based
# on file name and subdirectory name patterns, and then on file
# contents
action = findAllInFile
elif len(args) == 2:
# FIND_REGEX and REPLACE_PATTERN both supplied, so match based
# on file name and subdirectory name patterns, and then do a find and
# replace on file contents
action = replaceAllInFile
else:
raise Error(errno.EINVAL,'too many (%d) arguments supplied:\n%s' % (
len(args), ' '.join(args)))
return action, vars(options), args, parser
def _main(argv):
"""Wrapper that catches exceptions, prints output, and returns exit status.
Normal program output is printed to stdout. Error output (including
exception text) is printed to stderr.
Args:
argv: script arguments, usually sys.argv; argv[0] is expected to be the
program name
Returns:
exit code suitable for sys.exit()
"""
options = {} # empty options, used if _parseArgs() fails
parser = None
try:
action, options, args, parser = _parseArgs(argv[1:])
exit_code, output = applyActionToFiles(action, args, **options)
if output: print '\n'.join(output)
except Error, error:
if not options.get('quiet_output'):
print >>sys.stderr, '\nERROR: (%s: %s) %s\n' % (
error.args[0], os.strerror(error.args[0]), error.args[1])
if parser:
print >>sys.stderr, parser.get_usage()
exit_code = error.args[0]
return exit_code
if __name__ == '__main__':
sys.exit(_main(sys.argv))