|
|
@@ -0,0 +1,253 @@ |
|
|
import difflib |
|
|
import functools |
|
|
import getopt |
|
|
import io |
|
|
import locale |
|
|
import os |
|
|
import sys |
|
|
|
|
|
import util |
|
|
from util import to_string |
|
|
|
|
|
class DiffFlags(): |
|
|
def __init__(self): |
|
|
self.ignore_all_space = False |
|
|
self.ignore_space_change = False |
|
|
self.unified_diff = False |
|
|
self.num_context_lines = 3 |
|
|
self.recursive_diff = False |
|
|
self.strip_trailing_cr = False |
|
|
|
|
|
def getDirTree(path, basedir=""): |
|
|
# Tree is a tuple of form (dirname, child_trees). |
|
|
# An empty dir has child_trees = [], a file has child_trees = None. |
|
|
child_trees = [] |
|
|
for dirname, child_dirs, files in os.walk(os.path.join(basedir, path)): |
|
|
for child_dir in child_dirs: |
|
|
child_trees.append(getDirTree(child_dir, dirname)) |
|
|
for filename in files: |
|
|
child_trees.append((filename, None)) |
|
|
return path, sorted(child_trees) |
|
|
|
|
|
def compareTwoFiles(flags, filepaths): |
|
|
filelines = [] |
|
|
for file in filepaths: |
|
|
if file == "-": |
|
|
stdin_fileno = sys.stdin.fileno() |
|
|
with os.fdopen(os.dup(stdin_fileno), 'rb') as stdin_bin: |
|
|
filelines.append(stdin_bin.readlines()) |
|
|
else: |
|
|
with open(file, 'rb') as file_bin: |
|
|
filelines.append(file_bin.readlines()) |
|
|
|
|
|
try: |
|
|
return compareTwoTextFiles(flags, filepaths, filelines, |
|
|
locale.getpreferredencoding(False)) |
|
|
except UnicodeDecodeError: |
|
|
try: |
|
|
return compareTwoTextFiles(flags, filepaths, filelines, "utf-8") |
|
|
except: |
|
|
return compareTwoBinaryFiles(flags, filepaths, filelines) |
|
|
|
|
|
def compareTwoBinaryFiles(flags, filepaths, filelines): |
|
|
exitCode = 0 |
|
|
if hasattr(difflib, 'diff_bytes'): |
|
|
# python 3.5 or newer |
|
|
diffs = difflib.diff_bytes(difflib.unified_diff, filelines[0], |
|
|
filelines[1], filepaths[0].encode(), |
|
|
filepaths[1].encode(), |
|
|
n = flags.num_context_lines) |
|
|
diffs = [diff.decode(errors="backslashreplace") for diff in diffs] |
|
|
else: |
|
|
# python 2.7 |
|
|
if flags.unified_diff: |
|
|
func = difflib.unified_diff |
|
|
else: |
|
|
func = difflib.context_diff |
|
|
diffs = func(filelines[0], filelines[1], filepaths[0], filepaths[1], |
|
|
n = flags.num_context_lines) |
|
|
|
|
|
for diff in diffs: |
|
|
sys.stdout.write(to_string(diff)) |
|
|
exitCode = 1 |
|
|
return exitCode |
|
|
|
|
|
def compareTwoTextFiles(flags, filepaths, filelines_bin, encoding): |
|
|
filelines = [] |
|
|
for lines_bin in filelines_bin: |
|
|
lines = [] |
|
|
for line_bin in lines_bin: |
|
|
line = line_bin.decode(encoding=encoding) |
|
|
lines.append(line) |
|
|
filelines.append(lines) |
|
|
|
|
|
exitCode = 0 |
|
|
def compose2(f, g): |
|
|
return lambda x: f(g(x)) |
|
|
|
|
|
f = lambda x: x |
|
|
if flags.strip_trailing_cr: |
|
|
f = compose2(lambda line: line.replace('\r\n', '\n'), f) |
|
|
if flags.ignore_all_space or flags.ignore_space_change: |
|
|
ignoreSpace = lambda line, separator: separator.join(line.split()) |
|
|
ignoreAllSpaceOrSpaceChange = functools.partial(ignoreSpace, separator='' if flags.ignore_all_space else ' ') |
|
|
f = compose2(ignoreAllSpaceOrSpaceChange, f) |
|
|
|
|
|
for idx, lines in enumerate(filelines): |
|
|
filelines[idx]= [f(line) for line in lines] |
|
|
|
|
|
func = difflib.unified_diff if flags.unified_diff else difflib.context_diff |
|
|
for diff in func(filelines[0], filelines[1], filepaths[0], filepaths[1], |
|
|
n = flags.num_context_lines): |
|
|
sys.stdout.write(to_string(diff)) |
|
|
exitCode = 1 |
|
|
return exitCode |
|
|
|
|
|
def printDirVsFile(dir_path, file_path): |
|
|
if os.path.getsize(file_path): |
|
|
msg = "File %s is a directory while file %s is a regular file" |
|
|
else: |
|
|
msg = "File %s is a directory while file %s is a regular empty file" |
|
|
sys.stdout.write(msg % (dir_path, file_path) + "\n") |
|
|
|
|
|
def printFileVsDir(file_path, dir_path): |
|
|
if os.path.getsize(file_path): |
|
|
msg = "File %s is a regular file while file %s is a directory" |
|
|
else: |
|
|
msg = "File %s is a regular empty file while file %s is a directory" |
|
|
sys.stdout.write(msg % (file_path, dir_path) + "\n") |
|
|
|
|
|
def printOnlyIn(basedir, path, name): |
|
|
sys.stdout.write("Only in %s: %s\n" % (os.path.join(basedir, path), name)) |
|
|
|
|
|
def compareDirTrees(flags, dir_trees, base_paths=["", ""]): |
|
|
# Dirnames of the trees are not checked, it's caller's responsibility, |
|
|
# as top-level dirnames are always different. Base paths are important |
|
|
# for doing os.walk, but we don't put it into tree's dirname in order |
|
|
# to speed up string comparison below and while sorting in getDirTree. |
|
|
left_tree, right_tree = dir_trees[0], dir_trees[1] |
|
|
left_base, right_base = base_paths[0], base_paths[1] |
|
|
|
|
|
# Compare two files or report file vs. directory mismatch. |
|
|
if left_tree[1] is None and right_tree[1] is None: |
|
|
return compareTwoFiles(flags, |
|
|
[os.path.join(left_base, left_tree[0]), |
|
|
os.path.join(right_base, right_tree[0])]) |
|
|
|
|
|
if left_tree[1] is None and right_tree[1] is not None: |
|
|
printFileVsDir(os.path.join(left_base, left_tree[0]), |
|
|
os.path.join(right_base, right_tree[0])) |
|
|
return 1 |
|
|
|
|
|
if left_tree[1] is not None and right_tree[1] is None: |
|
|
printDirVsFile(os.path.join(left_base, left_tree[0]), |
|
|
os.path.join(right_base, right_tree[0])) |
|
|
return 1 |
|
|
|
|
|
# Compare two directories via recursive use of compareDirTrees. |
|
|
exitCode = 0 |
|
|
left_names = [node[0] for node in left_tree[1]] |
|
|
right_names = [node[0] for node in right_tree[1]] |
|
|
l, r = 0, 0 |
|
|
while l < len(left_names) and r < len(right_names): |
|
|
# Names are sorted in getDirTree, rely on that order. |
|
|
if left_names[l] < right_names[r]: |
|
|
exitCode = 1 |
|
|
printOnlyIn(left_base, left_tree[0], left_names[l]) |
|
|
l += 1 |
|
|
elif left_names[l] > right_names[r]: |
|
|
exitCode = 1 |
|
|
printOnlyIn(right_base, right_tree[0], right_names[r]) |
|
|
r += 1 |
|
|
else: |
|
|
exitCode |= compareDirTrees(flags, |
|
|
[left_tree[1][l], right_tree[1][r]], |
|
|
[os.path.join(left_base, left_tree[0]), |
|
|
os.path.join(right_base, right_tree[0])]) |
|
|
l += 1 |
|
|
r += 1 |
|
|
|
|
|
# At least one of the trees has ended. Report names from the other tree. |
|
|
while l < len(left_names): |
|
|
exitCode = 1 |
|
|
printOnlyIn(left_base, left_tree[0], left_names[l]) |
|
|
l += 1 |
|
|
while r < len(right_names): |
|
|
exitCode = 1 |
|
|
printOnlyIn(right_base, right_tree[0], right_names[r]) |
|
|
r += 1 |
|
|
return exitCode |
|
|
|
|
|
def main(argv): |
|
|
if sys.platform == "win32": |
|
|
if hasattr(sys.stdout, 'buffer'): |
|
|
# python 3 |
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, newline='\n') |
|
|
else: |
|
|
# python 2.7 |
|
|
import msvcrt |
|
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
|
|
args = argv[1:] |
|
|
try: |
|
|
opts, args = getopt.gnu_getopt(args, "wbuU:r", ["strip-trailing-cr"]) |
|
|
except getopt.GetoptError as err: |
|
|
sys.stderr.write("Unsupported: 'diff': %s\n" % str(err)) |
|
|
sys.exit(1) |
|
|
|
|
|
flags = DiffFlags() |
|
|
filelines, filepaths, dir_trees = ([] for i in range(3)) |
|
|
for o, a in opts: |
|
|
if o == "-w": |
|
|
flags.ignore_all_space = True |
|
|
elif o == "-b": |
|
|
flags.ignore_space_change = True |
|
|
elif o == "-u": |
|
|
flags.unified_diff = True |
|
|
elif o.startswith("-U"): |
|
|
flags.unified_diff = True |
|
|
try: |
|
|
flags.num_context_lines = int(a) |
|
|
if flags.num_context_lines < 0: |
|
|
raise ValueException |
|
|
except: |
|
|
sys.stderr.write("Error: invalid '-U' argument: {}\n" |
|
|
.format(a)) |
|
|
sys.exit(1) |
|
|
elif o == "-r": |
|
|
flags.recursive_diff = True |
|
|
elif o == "--strip-trailing-cr": |
|
|
flags.strip_trailing_cr = True |
|
|
else: |
|
|
assert False, "unhandled option" |
|
|
|
|
|
if len(args) != 2: |
|
|
sys.stderr.write("Error: missing or extra operand\n") |
|
|
sys.exit(1) |
|
|
|
|
|
exitCode = 0 |
|
|
try: |
|
|
for file in args: |
|
|
if file != "-" and not os.path.isabs(file): |
|
|
file = os.path.realpath(os.path.join(os.getcwd(), file)) |
|
|
|
|
|
if flags.recursive_diff: |
|
|
if file == "-": |
|
|
sys.stderr.write("Error: cannot recursively compare '-'\n") |
|
|
sys.exit(1) |
|
|
dir_trees.append(getDirTree(file)) |
|
|
else: |
|
|
filepaths.append(file) |
|
|
|
|
|
if not flags.recursive_diff: |
|
|
exitCode = compareTwoFiles(flags, filepaths) |
|
|
else: |
|
|
exitCode = compareDirTrees(flags, dir_trees) |
|
|
|
|
|
except IOError as err: |
|
|
sys.stderr.write("Error: 'diff' command failed, %s\n" % str(err)) |
|
|
exitCode = 1 |
|
|
|
|
|
sys.exit(exitCode) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main(sys.argv) |