13 | 13 |
with changes to provide console output instead of html output. """
|
14 | 14 |
|
15 | 15 |
import os
|
|
16 |
import stat
|
16 | 17 |
import sys
|
17 | 18 |
import errno
|
18 | 19 |
import difflib
|
|
22 | 23 |
import unicodedata
|
23 | 24 |
import codecs
|
24 | 25 |
|
25 | |
__version__ = "2.0.4"
|
|
26 |
__version__ = "2.0.5"
|
|
27 |
|
|
28 |
# Exit code constants
|
|
29 |
EXIT_CODE_SUCCESS = 0
|
|
30 |
EXIT_CODE_DIFF = 1
|
|
31 |
EXIT_CODE_ERROR = 2
|
26 | 32 |
|
27 | 33 |
color_codes = {
|
28 | |
"black": '\033[0;30m',
|
29 | |
"red": '\033[0;31m',
|
30 | |
"green": '\033[0;32m',
|
31 | |
"yellow": '\033[0;33m',
|
32 | |
"blue": '\033[0;34m',
|
33 | |
"magenta": '\033[0;35m',
|
34 | |
"cyan": '\033[0;36m',
|
35 | |
"white": '\033[0;37m',
|
36 | |
"none": '\033[m',
|
37 | |
"black_bold": '\033[1;30m',
|
38 | |
"red_bold": '\033[1;31m',
|
39 | |
"green_bold": '\033[1;32m',
|
40 | |
"yellow_bold": '\033[1;33m',
|
41 | |
"blue_bold": '\033[1;34m',
|
42 | |
"magenta_bold": '\033[1;35m',
|
43 | |
"cyan_bold": '\033[1;36m',
|
44 | |
"white_bold": '\033[1;37m',
|
|
34 |
"black": "\033[0;30m",
|
|
35 |
"red": "\033[0;31m",
|
|
36 |
"green": "\033[0;32m",
|
|
37 |
"yellow": "\033[0;33m",
|
|
38 |
"blue": "\033[0;34m",
|
|
39 |
"magenta": "\033[0;35m",
|
|
40 |
"cyan": "\033[0;36m",
|
|
41 |
"white": "\033[0;37m",
|
|
42 |
"none": "\033[m",
|
|
43 |
"black_bold": "\033[1;30m",
|
|
44 |
"red_bold": "\033[1;31m",
|
|
45 |
"green_bold": "\033[1;32m",
|
|
46 |
"yellow_bold": "\033[1;33m",
|
|
47 |
"blue_bold": "\033[1;34m",
|
|
48 |
"magenta_bold": "\033[1;35m",
|
|
49 |
"cyan_bold": "\033[1;36m",
|
|
50 |
"white_bold": "\033[1;37m",
|
45 | 51 |
}
|
46 | 52 |
|
47 | 53 |
|
|
51 | 57 |
"change": "yellow_bold",
|
52 | 58 |
"separator": "blue",
|
53 | 59 |
"description": "blue",
|
|
60 |
"permissions": "yellow",
|
54 | 61 |
"meta": "magenta",
|
55 | 62 |
"line-numbers": "white",
|
56 | 63 |
}
|
|
81 | 88 |
|
82 | 89 |
"""
|
83 | 90 |
|
84 | |
def __init__(self, tabsize=8, wrapcolumn=None, linejunk=None,
|
85 | |
charjunk=difflib.IS_CHARACTER_JUNK, cols=80,
|
86 | |
line_numbers=False,
|
87 | |
show_all_spaces=False,
|
88 | |
highlight=False,
|
89 | |
truncate=False,
|
90 | |
strip_trailing_cr=False):
|
|
91 |
def __init__(
|
|
92 |
self,
|
|
93 |
tabsize=8,
|
|
94 |
wrapcolumn=None,
|
|
95 |
linejunk=None,
|
|
96 |
charjunk=difflib.IS_CHARACTER_JUNK,
|
|
97 |
cols=80,
|
|
98 |
line_numbers=False,
|
|
99 |
show_all_spaces=False,
|
|
100 |
highlight=False,
|
|
101 |
truncate=False,
|
|
102 |
strip_trailing_cr=False,
|
|
103 |
):
|
91 | 104 |
"""ConsoleDiff instance initializer
|
92 | 105 |
|
93 | 106 |
Arguments:
|
|
127 | 140 |
spaces and vice versa. At the end of the table generation, the tab
|
128 | 141 |
characters will be replaced with a space.
|
129 | 142 |
"""
|
|
143 |
|
130 | 144 |
def expand_tabs(line):
|
131 | 145 |
# hide real spaces
|
132 | |
line = line.replace(' ', '\0')
|
|
146 |
line = line.replace(" ", "\0")
|
133 | 147 |
# expand tabs into spaces
|
134 | 148 |
line = line.expandtabs(self._tabsize)
|
135 | 149 |
# replace spaces from expanded tabs back into tab characters
|
136 | 150 |
# (we'll replace them with markup after we do differencing)
|
137 | |
line = line.replace(' ', '\t')
|
138 | |
return line.replace('\0', ' ').rstrip('\n')
|
|
151 |
line = line.replace(" ", "\t")
|
|
152 |
return line.replace("\0", " ").rstrip("\n")
|
|
153 |
|
139 | 154 |
fromlines = [expand_tabs(line) for line in fromlines]
|
140 | 155 |
tolines = [expand_tabs(line) for line in tolines]
|
141 | 156 |
return fromlines, tolines
|
142 | 157 |
|
143 | 158 |
def _strip_trailing_cr(self, lines):
|
144 | |
""" Remove windows return carriage
|
145 | |
"""
|
146 | |
lines = [line.rstrip('\r') for line in lines]
|
|
159 |
"""Remove windows return carriage"""
|
|
160 |
lines = [line.rstrip("\r") for line in lines]
|
147 | 161 |
return lines
|
148 | 162 |
|
149 | 163 |
def _all_cr_nl(self, lines):
|
150 | |
""" Whether a file is entirely \r\n line endings
|
151 | |
"""
|
152 | |
return all(line.endswith('\r') for line in lines)
|
|
164 |
"""Whether a file is entirely \r\n line endings"""
|
|
165 |
return all(line.endswith("\r") for line in lines)
|
153 | 166 |
|
154 | 167 |
def _display_len(self, s):
|
155 | 168 |
# Handle wide characters like Chinese.
|
156 | 169 |
def width(c):
|
157 | |
if ((isinstance(c, type(u"")) and
|
158 | |
unicodedata.east_asian_width(c) == 'W')):
|
|
170 |
if (
|
|
171 |
isinstance(c, type(""))
|
|
172 |
and unicodedata.east_asian_width(c) == "W"
|
|
173 |
):
|
159 | 174 |
return 2
|
160 | |
elif c == '\r':
|
|
175 |
elif c == "\r":
|
161 | 176 |
return 2
|
162 | 177 |
return 1
|
163 | 178 |
|
|
182 | 197 |
|
183 | 198 |
# if line text doesn't need wrapping, just add it to the output
|
184 | 199 |
# list
|
185 | |
if ((self._display_len(text) - (text.count('\0') * 3) <=
|
186 | |
self._wrapcolumn)):
|
|
200 |
if (
|
|
201 |
self._display_len(text) - (text.count("\0") * 3)
|
|
202 |
<= self._wrapcolumn
|
|
203 |
):
|
187 | 204 |
data_list.append((line_num, text))
|
188 | 205 |
return
|
189 | 206 |
|
|
191 | 208 |
# point is inside markers
|
192 | 209 |
i = 0
|
193 | 210 |
n = 0
|
194 | |
mark = ''
|
|
211 |
mark = ""
|
195 | 212 |
while n < self._wrapcolumn and i < len(text):
|
196 | |
if text[i] == '\0':
|
|
213 |
if text[i] == "\0":
|
197 | 214 |
i += 1
|
198 | 215 |
mark = text[i]
|
199 | 216 |
i += 1
|
200 | |
elif text[i] == '\1':
|
|
217 |
elif text[i] == "\1":
|
201 | 218 |
i += 1
|
202 | |
mark = ''
|
|
219 |
mark = ""
|
203 | 220 |
else:
|
204 | 221 |
n += self._display_len(text[i])
|
205 | 222 |
i += 1
|
|
212 | 229 |
# line and start marker at beginning of second line because each
|
213 | 230 |
# line will have its own table tag markup around it.
|
214 | 231 |
if mark:
|
215 | |
line1 = line1 + '\1'
|
216 | |
line2 = '\0' + mark + line2
|
|
232 |
line1 = line1 + "\1"
|
|
233 |
line2 = "\0" + mark + line2
|
217 | 234 |
|
218 | 235 |
# tack on first line onto the output list
|
219 | 236 |
data_list.append((line_num, line1))
|
|
222 | 239 |
# unless truncate is set
|
223 | 240 |
if self.truncate:
|
224 | 241 |
return
|
225 | |
line_num = '>'
|
|
242 |
line_num = ">"
|
226 | 243 |
text = line2
|
227 | 244 |
|
228 | 245 |
def _line_wrapper(self, diffs):
|
|
246 | 263 |
if fromlist:
|
247 | 264 |
fromdata = fromlist.pop(0)
|
248 | 265 |
else:
|
249 | |
fromdata = ('', ' ')
|
|
266 |
fromdata = ("", " ")
|
250 | 267 |
if tolist:
|
251 | 268 |
todata = tolist.pop(0)
|
252 | 269 |
else:
|
253 | |
todata = ('', ' ')
|
|
270 |
todata = ("", " ")
|
254 | 271 |
yield fromdata, todata, flag
|
255 | 272 |
|
256 | 273 |
def _collect_lines(self, diffs):
|
|
265 | 282 |
if (fromdata, todata, flag) == (None, None, None):
|
266 | 283 |
yield None
|
267 | 284 |
else:
|
268 | |
yield (self._format_line(*fromdata),
|
269 | |
self._format_line(*todata))
|
|
285 |
yield (
|
|
286 |
self._format_line(*fromdata),
|
|
287 |
self._format_line(*todata),
|
|
288 |
)
|
270 | 289 |
|
271 | 290 |
def _format_line(self, linenum, text):
|
272 | 291 |
text = text.rstrip()
|
|
276 | 295 |
|
277 | 296 |
def _add_line_numbers(self, linenum, text):
|
278 | 297 |
try:
|
279 | |
lid = '%d' % linenum
|
|
298 |
lid = "%d" % linenum
|
280 | 299 |
except TypeError:
|
281 | 300 |
# handle blank lines where linenum is '>' or ''
|
282 | |
lid = ''
|
|
301 |
lid = ""
|
283 | 302 |
return text
|
284 | |
return '%s %s' % (
|
285 | |
self._rpad(simple_colorize(str(lid), "line-numbers"), 8), text)
|
|
303 |
return "%s %s" % (
|
|
304 |
self._rpad(simple_colorize(str(lid), "line-numbers"), 8),
|
|
305 |
text,
|
|
306 |
)
|
286 | 307 |
|
287 | 308 |
def _real_len(self, s):
|
288 | 309 |
s_len = 0
|
289 | 310 |
in_esc = False
|
290 | |
prev = ' '
|
291 | |
for c in replace_all({'\0+': "",
|
292 | |
'\0-': "",
|
293 | |
'\0^': "",
|
294 | |
'\1': "",
|
295 | |
'\t': ' '}, s):
|
|
311 |
prev = " "
|
|
312 |
for c in replace_all(
|
|
313 |
{"\0+": "", "\0-": "", "\0^": "", "\1": "", "\t": " "}, s
|
|
314 |
):
|
296 | 315 |
if in_esc:
|
297 | 316 |
if c == "m":
|
298 | 317 |
in_esc = False
|
|
315 | 334 |
def _lpad(self, s, field_width):
|
316 | 335 |
return s + self._pad(s, field_width)
|
317 | 336 |
|
318 | |
def make_table(self, fromlines, tolines, fromdesc='', todesc='',
|
319 | |
context=False, numlines=5):
|
|
337 |
def make_table(
|
|
338 |
self,
|
|
339 |
fromlines,
|
|
340 |
tolines,
|
|
341 |
fromdesc="",
|
|
342 |
todesc="",
|
|
343 |
fromperms=None,
|
|
344 |
toperms=None,
|
|
345 |
context=False,
|
|
346 |
numlines=5,
|
|
347 |
):
|
320 | 348 |
"""Generates table of side by side comparison with change highlights
|
321 | 349 |
|
322 | 350 |
Arguments:
|
|
324 | 352 |
tolines -- list of "to" lines
|
325 | 353 |
fromdesc -- "from" file column header string
|
326 | 354 |
todesc -- "to" file column header string
|
|
355 |
fromperms -- "from" file permissions
|
|
356 |
toperms -- "to" file permissions
|
327 | 357 |
context -- set to True for contextual differences (defaults to False
|
328 | 358 |
which shows full differences).
|
329 | 359 |
numlines -- number of context lines. When context is set True,
|
|
342 | 372 |
fromlines, tolines = self._tab_newline_replace(fromlines, tolines)
|
343 | 373 |
|
344 | 374 |
if self.strip_trailing_cr or (
|
345 | |
self._all_cr_nl(fromlines) and self._all_cr_nl(tolines)):
|
|
375 |
self._all_cr_nl(fromlines) and self._all_cr_nl(tolines)
|
|
376 |
):
|
346 | 377 |
fromlines = self._strip_trailing_cr(fromlines)
|
347 | 378 |
tolines = self._strip_trailing_cr(tolines)
|
348 | 379 |
|
349 | 380 |
# create diffs iterator which generates side by side from/to data
|
350 | |
diffs = difflib._mdiff(fromlines, tolines, context_lines,
|
351 | |
linejunk=self._linejunk,
|
352 | |
charjunk=self._charjunk)
|
|
381 |
diffs = difflib._mdiff(
|
|
382 |
fromlines,
|
|
383 |
tolines,
|
|
384 |
context_lines,
|
|
385 |
linejunk=self._linejunk,
|
|
386 |
charjunk=self._charjunk,
|
|
387 |
)
|
353 | 388 |
|
354 | 389 |
# set up iterator to wrap lines that exceed desired width
|
355 | 390 |
if self._wrapcolumn:
|
356 | 391 |
diffs = self._line_wrapper(diffs)
|
357 | 392 |
diffs = self._collect_lines(diffs)
|
358 | 393 |
|
359 | |
for left, right in self._generate_table(fromdesc, todesc, diffs):
|
|
394 |
for left, right in self._generate_table(
|
|
395 |
fromdesc, todesc, fromperms, toperms, diffs
|
|
396 |
):
|
360 | 397 |
yield self.colorize(
|
361 | |
"%s %s" % (self._lpad(left, self.cols // 2 - 1),
|
362 | |
self._lpad(right, self.cols // 2 - 1)))
|
363 | |
|
364 | |
def _generate_table(self, fromdesc, todesc, diffs):
|
|
398 |
"%s %s"
|
|
399 |
% (
|
|
400 |
self._lpad(left, self.cols // 2 - 1),
|
|
401 |
self._lpad(right, self.cols // 2 - 1),
|
|
402 |
)
|
|
403 |
)
|
|
404 |
|
|
405 |
def _generate_table(self, fromdesc, todesc, fromperms, toperms, diffs):
|
365 | 406 |
if fromdesc or todesc:
|
366 | |
yield (simple_colorize(fromdesc, "description"),
|
367 | |
simple_colorize(todesc, "description"))
|
|
407 |
yield (
|
|
408 |
simple_colorize(fromdesc, "description"),
|
|
409 |
simple_colorize(todesc, "description"),
|
|
410 |
)
|
|
411 |
|
|
412 |
if fromperms != toperms:
|
|
413 |
yield (
|
|
414 |
simple_colorize(
|
|
415 |
f"{stat.filemode(fromperms)} ({fromperms:o})",
|
|
416 |
"permissions",
|
|
417 |
),
|
|
418 |
simple_colorize(
|
|
419 |
f"{stat.filemode(toperms)} ({toperms:o})", "permissions"
|
|
420 |
),
|
|
421 |
)
|
368 | 422 |
|
369 | 423 |
for i, line in enumerate(diffs):
|
370 | 424 |
if line is None:
|
371 | 425 |
# mdiff yields None on separator lines; skip the bogus ones
|
372 | 426 |
# generated for the first line
|
373 | 427 |
if i > 0:
|
374 | |
yield (simple_colorize('---', "separator"),
|
375 | |
simple_colorize('---', "separator"))
|
|
428 |
yield (
|
|
429 |
simple_colorize("---", "separator"),
|
|
430 |
simple_colorize("---", "separator"),
|
|
431 |
)
|
376 | 432 |
else:
|
377 | 433 |
yield line
|
378 | 434 |
|
379 | 435 |
def colorize(self, s):
|
380 | 436 |
def background(color):
|
381 | |
return replace_all({"\033[1;": "\033[7;1;",
|
382 | |
"\033[0;": "\033[7;"}, color)
|
|
437 |
return replace_all(
|
|
438 |
{"\033[1;": "\033[7;1;", "\033[0;": "\033[7;"}, color
|
|
439 |
)
|
383 | 440 |
|
384 | 441 |
C_ADD = color_codes[color_mapping["add"]]
|
385 | 442 |
C_SUB = color_codes[color_mapping["subtract"]]
|
386 | 443 |
C_CHG = color_codes[color_mapping["change"]]
|
387 | 444 |
|
388 | 445 |
if self.highlight:
|
389 | |
C_ADD, C_SUB, C_CHG = (background(C_ADD),
|
390 | |
background(C_SUB),
|
391 | |
background(C_CHG))
|
|
446 |
C_ADD, C_SUB, C_CHG = (
|
|
447 |
background(C_ADD),
|
|
448 |
background(C_SUB),
|
|
449 |
background(C_CHG),
|
|
450 |
)
|
392 | 451 |
|
393 | 452 |
C_NONE = color_codes["none"]
|
394 | 453 |
colors = (C_ADD, C_SUB, C_CHG, C_NONE)
|
395 | 454 |
|
396 | |
s = replace_all({'\0+': C_ADD,
|
397 | |
'\0-': C_SUB,
|
398 | |
'\0^': C_CHG,
|
399 | |
'\1': C_NONE,
|
400 | |
'\t': ' ',
|
401 | |
'\r': '\\r'}, s)
|
|
455 |
s = replace_all(
|
|
456 |
{
|
|
457 |
"\0+": C_ADD,
|
|
458 |
"\0-": C_SUB,
|
|
459 |
"\0^": C_CHG,
|
|
460 |
"\1": C_NONE,
|
|
461 |
"\t": " ",
|
|
462 |
"\r": "\\r",
|
|
463 |
},
|
|
464 |
s,
|
|
465 |
)
|
402 | 466 |
|
403 | 467 |
if self.highlight:
|
404 | 468 |
return s
|
|
406 | 470 |
if not self.show_all_spaces:
|
407 | 471 |
# If there's a change consisting entirely of whitespace,
|
408 | 472 |
# don't color it.
|
409 | |
return re.sub("\033\\[[01];3([01234567])m(\\s+)(\033\\[)",
|
410 | |
"\033[7;3\\1m\\2\\3", s)
|
|
473 |
return re.sub(
|
|
474 |
"\033\\[[01];3([01234567])m(\\s+)(\033\\[)",
|
|
475 |
"\033[7;3\\1m\\2\\3",
|
|
476 |
s,
|
|
477 |
)
|
411 | 478 |
|
412 | 479 |
def will_see_coloredspace(i, s):
|
413 | 480 |
while i < len(s) and s[i].isspace():
|
414 | 481 |
i += 1
|
415 | |
if i < len(s) and s[i] == '\033':
|
|
482 |
if i < len(s) and s[i] == "\033":
|
416 | 483 |
return False
|
417 | 484 |
return True
|
418 | 485 |
|
|
430 | 497 |
if ns_end.endswith(C_NONE):
|
431 | 498 |
in_color = False
|
432 | 499 |
|
433 | |
if ((c.isspace() and in_color and
|
434 | |
(self.show_all_spaces or not (seen_coloredspace or
|
435 | |
will_see_coloredspace(i, s))))):
|
|
500 |
if (
|
|
501 |
c.isspace()
|
|
502 |
and in_color
|
|
503 |
and (
|
|
504 |
self.show_all_spaces
|
|
505 |
or not (seen_coloredspace or will_see_coloredspace(i, s))
|
|
506 |
)
|
|
507 |
):
|
436 | 508 |
n_s.extend([C_NONE, background(in_color), c, C_NONE, in_color])
|
437 | 509 |
else:
|
438 | 510 |
if in_color:
|
|
473 | 545 |
|
474 | 546 |
def create_option_parser():
|
475 | 547 |
# If you change any of these, also update README.
|
476 | |
parser = OptionParser(usage="usage: %prog [options] left_file right_file",
|
477 | |
version="icdiff version %s" % __version__,
|
478 | |
description="Show differences between files in a "
|
479 | |
"two column view.",
|
480 | |
option_class=MultipleOption)
|
481 | |
parser.add_option("--cols", default=None,
|
482 | |
help="specify the width of the screen. Autodetection is "
|
483 | |
"Unix only")
|
484 | |
parser.add_option("--encoding", default="utf-8",
|
485 | |
help="specify the file encoding; defaults to utf8")
|
486 | |
parser.add_option("-E", "--exclude-lines",
|
487 | |
action="store",
|
488 | |
type="string",
|
489 | |
dest='matcher',
|
490 | |
help="Do not diff lines that match this regex. Not "
|
491 | |
"compatible with the 'line-numbers' option")
|
492 | |
parser.add_option("--head", default=0,
|
493 | |
help="consider only the first N lines of each file")
|
494 | |
parser.add_option("-H", "--highlight", default=False,
|
495 | |
action="store_true",
|
496 | |
help="color by changing the background color instead of "
|
497 | |
"the foreground color. Very fast, ugly, displays all "
|
498 | |
"changes")
|
499 | |
parser.add_option("-L", "--label",
|
500 | |
action="extend",
|
501 | |
type="string",
|
502 | |
dest='labels',
|
503 | |
help="override file labels with arbitrary tags. "
|
504 | |
"Use twice, one for each file. You may include the "
|
505 | |
"formatting strings '{path}' and '{basename}'")
|
506 | |
parser.add_option("-N", "--line-numbers", default=False,
|
507 | |
action="store_true",
|
508 | |
help="generate output with line numbers. Not compatible "
|
509 | |
"with the 'exclude-lines' option.")
|
510 | |
parser.add_option("--no-bold", default=False,
|
511 | |
action="store_true",
|
512 | |
help="use non-bold colors; recommended for solarized")
|
513 | |
parser.add_option("--no-headers", default=False,
|
514 | |
action="store_true",
|
515 | |
help="don't label the left and right sides "
|
516 | |
"with their file names")
|
517 | |
parser.add_option("--output-encoding", default="utf-8",
|
518 | |
help="specify the output encoding; defaults to utf8")
|
519 | |
parser.add_option("-r", "--recursive", default=False,
|
520 | |
action="store_true",
|
521 | |
help="recursively compare subdirectories")
|
522 | |
parser.add_option("-s", "--report-identical-files", default=False,
|
523 | |
action="store_true",
|
524 | |
help="report when two files are the same")
|
525 | |
parser.add_option("--show-all-spaces", default=False,
|
526 | |
action="store_true",
|
527 | |
help="color all non-matching whitespace including "
|
528 | |
"that which is not needed for drawing the eye to "
|
529 | |
"changes. Slow, ugly, displays all changes")
|
530 | |
parser.add_option("--tabsize", default=8,
|
531 | |
help="tab stop spacing")
|
532 | |
parser.add_option("-t", "--truncate", default=False,
|
533 | |
action="store_true",
|
534 | |
help="truncate long lines instead of wrapping them")
|
535 | |
parser.add_option("-u", "--patch", default=True,
|
536 | |
action="store_true",
|
537 | |
help="generate patch. This is always true, "
|
538 | |
"and only exists for compatibility")
|
539 | |
parser.add_option("-U", "--unified", "--numlines", default=5,
|
540 | |
metavar="NUM",
|
541 | |
help="how many lines of context to print; "
|
542 | |
"can't be combined with --whole-file")
|
543 | |
parser.add_option("-W", "--whole-file", default=False,
|
544 | |
action="store_true",
|
545 | |
help="show the whole file instead of just changed "
|
546 | |
"lines and context")
|
547 | |
parser.add_option("--strip-trailing-cr", default=False,
|
548 | |
action="store_true",
|
549 | |
help="strip any trailing carriage return at the end of "
|
550 | |
"an input line")
|
551 | |
parser.add_option("--color-map", default=None,
|
552 | |
help="choose which colors are used for which items. "
|
553 | |
"Default is --color-map='" +
|
554 | |
",".join("%s:%s" % x
|
555 | |
for x in sorted(color_mapping.items())) + "'"
|
556 | |
". You don't have to override all of them: "
|
557 | |
"'--color-map=separator:white,description:cyan")
|
558 | |
parser.add_option("--is-git-diff", default=False,
|
559 | |
action="store_true",
|
560 | |
help="Show the real file name when displaying "
|
561 | |
"git-diff result")
|
|
548 |
parser = OptionParser(
|
|
549 |
usage="usage: %prog [options] left_file right_file",
|
|
550 |
version="icdiff version %s" % __version__,
|
|
551 |
description="Show differences between files in a " "two column view.",
|
|
552 |
option_class=MultipleOption,
|
|
553 |
)
|
|
554 |
parser.add_option(
|
|
555 |
"--cols",
|
|
556 |
default=None,
|
|
557 |
help="specify the width of the screen. Autodetection is " "Unix only",
|
|
558 |
)
|
|
559 |
parser.add_option(
|
|
560 |
"--encoding",
|
|
561 |
default="utf-8",
|
|
562 |
help="specify the file encoding; defaults to utf8",
|
|
563 |
)
|
|
564 |
parser.add_option(
|
|
565 |
"-E",
|
|
566 |
"--exclude-lines",
|
|
567 |
action="store",
|
|
568 |
type="string",
|
|
569 |
dest="matcher",
|
|
570 |
help="Do not diff lines that match this regex. Not "
|
|
571 |
"compatible with the 'line-numbers' option",
|
|
572 |
)
|
|
573 |
parser.add_option(
|
|
574 |
"--head",
|
|
575 |
default=0,
|
|
576 |
help="consider only the first N lines of each file",
|
|
577 |
)
|
|
578 |
parser.add_option(
|
|
579 |
"-H",
|
|
580 |
"--highlight",
|
|
581 |
default=False,
|
|
582 |
action="store_true",
|
|
583 |
help="color by changing the background color instead of "
|
|
584 |
"the foreground color. Very fast, ugly, displays all "
|
|
585 |
"changes",
|
|
586 |
)
|
|
587 |
parser.add_option(
|
|
588 |
"-L",
|
|
589 |
"--label",
|
|
590 |
action="extend",
|
|
591 |
type="string",
|
|
592 |
dest="labels",
|
|
593 |
help="override file labels with arbitrary tags. "
|
|
594 |
"Use twice, one for each file. You may include the "
|
|
595 |
"formatting strings '{path}' and '{basename}'",
|
|
596 |
)
|
|
597 |
parser.add_option(
|
|
598 |
"-N",
|
|
599 |
"--line-numbers",
|
|
600 |
default=False,
|
|
601 |
action="store_true",
|
|
602 |
help="generate output with line numbers. Not compatible "
|
|
603 |
"with the 'exclude-lines' option.",
|
|
604 |
)
|
|
605 |
parser.add_option(
|
|
606 |
"--no-bold",
|
|
607 |
default=False,
|
|
608 |
action="store_true",
|
|
609 |
help="use non-bold colors; recommended for solarized",
|
|
610 |
)
|
|
611 |
parser.add_option(
|
|
612 |
"--no-headers",
|
|
613 |
default=False,
|
|
614 |
action="store_true",
|
|
615 |
help="don't label the left and right sides " "with their file names",
|
|
616 |
)
|
|
617 |
parser.add_option(
|
|
618 |
"--output-encoding",
|
|
619 |
default="utf-8",
|
|
620 |
help="specify the output encoding; defaults to utf8",
|
|
621 |
)
|
|
622 |
parser.add_option(
|
|
623 |
"-r",
|
|
624 |
"--recursive",
|
|
625 |
default=False,
|
|
626 |
action="store_true",
|
|
627 |
help="recursively compare subdirectories",
|
|
628 |
)
|
|
629 |
parser.add_option(
|
|
630 |
"-s",
|
|
631 |
"--report-identical-files",
|
|
632 |
default=False,
|
|
633 |
action="store_true",
|
|
634 |
help="report when two files are the same",
|
|
635 |
)
|
|
636 |
parser.add_option(
|
|
637 |
"--show-all-spaces",
|
|
638 |
default=False,
|
|
639 |
action="store_true",
|
|
640 |
help="color all non-matching whitespace including "
|
|
641 |
"that which is not needed for drawing the eye to "
|
|
642 |
"changes. Slow, ugly, displays all changes",
|
|
643 |
)
|
|
644 |
parser.add_option("--tabsize", default=8, help="tab stop spacing")
|
|
645 |
parser.add_option(
|
|
646 |
"-t",
|
|
647 |
"--truncate",
|
|
648 |
default=False,
|
|
649 |
action="store_true",
|
|
650 |
help="truncate long lines instead of wrapping them",
|
|
651 |
)
|
|
652 |
parser.add_option(
|
|
653 |
"-u",
|
|
654 |
"--patch",
|
|
655 |
default=True,
|
|
656 |
action="store_true",
|
|
657 |
help="generate patch. This is always true, "
|
|
658 |
"and only exists for compatibility",
|
|
659 |
)
|
|
660 |
parser.add_option(
|
|
661 |
"-U",
|
|
662 |
"--unified",
|
|
663 |
"--numlines",
|
|
664 |
default=5,
|
|
665 |
metavar="NUM",
|
|
666 |
help="how many lines of context to print; "
|
|
667 |
"can't be combined with --whole-file",
|
|
668 |
)
|
|
669 |
parser.add_option(
|
|
670 |
"-W",
|
|
671 |
"--whole-file",
|
|
672 |
default=False,
|
|
673 |
action="store_true",
|
|
674 |
help="show the whole file instead of just changed "
|
|
675 |
"lines and context",
|
|
676 |
)
|
|
677 |
parser.add_option(
|
|
678 |
"-P",
|
|
679 |
"--permissions",
|
|
680 |
default=False,
|
|
681 |
action="store_true",
|
|
682 |
help="compare the file permissions as well as the "
|
|
683 |
"content of the file",
|
|
684 |
)
|
|
685 |
parser.add_option(
|
|
686 |
"--strip-trailing-cr",
|
|
687 |
default=False,
|
|
688 |
action="store_true",
|
|
689 |
help="strip any trailing carriage return at the end of "
|
|
690 |
"an input line",
|
|
691 |
)
|
|
692 |
parser.add_option(
|
|
693 |
"--color-map",
|
|
694 |
default=None,
|
|
695 |
help="choose which colors are used for which items. "
|
|
696 |
"Default is --color-map='"
|
|
697 |
+ ",".join("%s:%s" % x for x in sorted(color_mapping.items()))
|
|
698 |
+ "'"
|
|
699 |
". You don't have to override all of them: "
|
|
700 |
"'--color-map=separator:white,description:cyan",
|
|
701 |
)
|
|
702 |
parser.add_option(
|
|
703 |
"--is-git-diff",
|
|
704 |
default=False,
|
|
705 |
action="store_true",
|
|
706 |
help="Show the real file name when displaying " "git-diff result",
|
|
707 |
)
|
562 | 708 |
return parser
|
563 | 709 |
|
564 | 710 |
|
565 | 711 |
def set_cols_option(options):
|
566 | |
if os.name == 'nt':
|
|
712 |
if os.name == "nt":
|
567 | 713 |
try:
|
568 | 714 |
import struct
|
569 | 715 |
from ctypes import windll, create_string_buffer
|
|
579 | 725 |
pass
|
580 | 726 |
|
581 | 727 |
else:
|
|
728 |
|
582 | 729 |
def ioctl_GWINSZ(fd):
|
583 | 730 |
try:
|
584 | 731 |
import fcntl
|
585 | 732 |
import termios
|
586 | 733 |
import struct
|
587 | |
cr = struct.unpack('hh', fcntl.ioctl(fd,
|
588 | |
termios.TIOCGWINSZ,
|
589 | |
'1234'))
|
|
734 |
|
|
735 |
cr = struct.unpack(
|
|
736 |
"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")
|
|
737 |
)
|
590 | 738 |
except Exception:
|
591 | 739 |
return None
|
592 | 740 |
return cr
|
|
741 |
|
593 | 742 |
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
594 | 743 |
if cr and cr[1] > 0:
|
595 | 744 |
options.cols = cr[1]
|
|
600 | 749 |
def validate_has_two_arguments(parser, args):
|
601 | 750 |
if len(args) != 2:
|
602 | 751 |
parser.print_help()
|
603 | |
sys.exit()
|
|
752 |
sys.exit(EXIT_CODE_DIFF)
|
604 | 753 |
|
605 | 754 |
|
606 | 755 |
def start():
|
|
756 |
diffs_found = False
|
607 | 757 |
parser = create_option_parser()
|
608 | 758 |
options, args = parser.parse_args()
|
609 | 759 |
validate_has_two_arguments(parser, args)
|
610 | 760 |
if not options.cols:
|
611 | 761 |
set_cols_option(options)
|
612 | 762 |
try:
|
613 | |
diff(options, *args)
|
|
763 |
diffs_found = diff(options, *args)
|
614 | 764 |
except KeyboardInterrupt:
|
615 | 765 |
pass
|
616 | 766 |
except IOError as e:
|
|
625 | 775 |
# See: https://stackoverflow.com/questions/26692284/...
|
626 | 776 |
# ...how-to-prevent-brokenpipeerror-when-doing-a-flush-in-python
|
627 | 777 |
sys.stderr.close()
|
|
778 |
sys.exit(EXIT_CODE_DIFF if diffs_found else EXIT_CODE_SUCCESS)
|
628 | 779 |
|
629 | 780 |
|
630 | 781 |
def codec_print(s, options):
|
|
635 | 786 |
sys.stdout.write(s.encode(options.output_encoding))
|
636 | 787 |
|
637 | 788 |
|
|
789 |
def cmp_perms(options, a, b):
|
|
790 |
return not options.permissions or (
|
|
791 |
os.lstat(a).st_mode == os.lstat(b).st_mode
|
|
792 |
)
|
|
793 |
|
|
794 |
|
638 | 795 |
def diff(options, a, b):
|
639 | 796 |
def print_meta(s):
|
640 | 797 |
codec_print(simple_colorize(s, "meta"), options)
|
|
798 |
|
|
799 |
# We start out and assume that no diffs have been found (so far)
|
|
800 |
diffs_found = False
|
641 | 801 |
|
642 | 802 |
# Don't use os.path.isfile; it returns False for file-like entities like
|
643 | 803 |
# bash's process substitution (/dev/fd/N).
|
|
646 | 806 |
|
647 | 807 |
if is_a_file and is_b_file:
|
648 | 808 |
try:
|
649 | |
if not filecmp.cmp(a, b, shallow=False):
|
650 | |
diff_files(options, a, b)
|
|
809 |
if not (
|
|
810 |
filecmp.cmp(a, b, shallow=False) and cmp_perms(options, a, b)
|
|
811 |
):
|
|
812 |
diffs_found = diffs_found | diff_files(options, a, b)
|
651 | 813 |
elif options.report_identical_files:
|
652 | 814 |
print("Files %s and %s are identical." % (a, b))
|
653 | 815 |
except OSError as e:
|
654 | 816 |
if e.errno == errno.ENOENT:
|
655 | 817 |
print_meta("error: file '%s' was not found" % e.filename)
|
|
818 |
sys.exit(EXIT_CODE_ERROR)
|
656 | 819 |
else:
|
657 | |
raise(e)
|
|
820 |
raise (e)
|
658 | 821 |
|
659 | 822 |
elif not is_a_file and not is_b_file:
|
660 | 823 |
a_contents = set(os.listdir(a))
|
|
666 | 829 |
elif child not in a_contents:
|
667 | 830 |
print_meta("Only in %s: %s" % (b, child))
|
668 | 831 |
elif options.recursive:
|
669 | |
diff(options,
|
670 | |
os.path.join(a, child),
|
671 | |
os.path.join(b, child))
|
|
832 |
diffs_found = diffs_found | diff(
|
|
833 |
options, os.path.join(a, child), os.path.join(b, child)
|
|
834 |
)
|
672 | 835 |
elif not is_a_file and is_b_file:
|
673 | 836 |
print_meta("File %s is a directory while %s is a file" % (a, b))
|
|
837 |
diffs_found = True
|
674 | 838 |
|
675 | 839 |
elif is_a_file and not is_b_file:
|
676 | 840 |
print_meta("File %s is a file while %s is a directory" % (a, b))
|
|
841 |
diffs_found = True
|
|
842 |
|
|
843 |
return diffs_found
|
677 | 844 |
|
678 | 845 |
|
679 | 846 |
def read_file(fname, options):
|
|
682 | 849 |
return inf.readlines()
|
683 | 850 |
except UnicodeDecodeError as e:
|
684 | 851 |
codec_print(
|
685 | |
"error: file '%s' not valid with encoding '%s': <%s> at %s-%s." %
|
686 | |
(fname, options.encoding, e.reason, e.start, e.end), options)
|
|
852 |
"error: file '%s' not valid with encoding '%s': <%s> at %s-%s."
|
|
853 |
% (fname, options.encoding, e.reason, e.start, e.end),
|
|
854 |
options,
|
|
855 |
)
|
687 | 856 |
raise
|
688 | 857 |
except LookupError:
|
689 | 858 |
codec_print(
|
690 | |
"error: encoding '%s' was not found." %
|
691 | |
(options.encoding), options)
|
692 | |
sys.exit()
|
|
859 |
"error: encoding '%s' was not found." % (options.encoding), options
|
|
860 |
)
|
|
861 |
sys.exit(EXIT_CODE_ERROR)
|
693 | 862 |
|
694 | 863 |
|
695 | 864 |
def format_label(path, label="{path}"):
|
|
697 | 866 |
|
698 | 867 |
Example:
|
699 | 868 |
For file `/foo/bar.py` and label "Yours: {basename}" -
|
700 | |
The ouptut is "Yours: bar.py"
|
|
869 |
The output is "Yours: bar.py"
|
701 | 870 |
"""
|
702 | 871 |
return label.format(path=path, basename=os.path.basename(path))
|
703 | 872 |
|
704 | 873 |
|
705 | 874 |
def diff_files(options, a, b):
|
|
875 |
diff_found = False
|
706 | 876 |
if options.is_git_diff is True:
|
707 | 877 |
# Use $BASE as label when displaying git-diff result
|
708 | 878 |
base = os.getenv("BASE")
|
709 | |
headers = [
|
710 | |
format_label(a, base),
|
711 | |
format_label(b, base)
|
712 | |
]
|
|
879 |
headers = [format_label(a, base), format_label(b, base)]
|
713 | 880 |
else:
|
714 | 881 |
if options.labels:
|
715 | 882 |
if len(options.labels) == 2:
|
|
718 | 885 |
format_label(b, options.labels[1]),
|
719 | 886 |
]
|
720 | 887 |
else:
|
721 | |
codec_print("error: to use arbitrary file labels, "
|
722 | |
"specify -L twice.", options)
|
723 | |
return
|
|
888 |
codec_print(
|
|
889 |
"error: to use arbitrary file labels, "
|
|
890 |
"specify -L twice.",
|
|
891 |
options,
|
|
892 |
)
|
|
893 |
sys.exit(EXIT_CODE_ERROR)
|
724 | 894 |
else:
|
725 | 895 |
headers = a, b
|
726 | 896 |
if options.no_headers:
|
|
735 | 905 |
lines_a = read_file(a, options)
|
736 | 906 |
lines_b = read_file(b, options)
|
737 | 907 |
except UnicodeDecodeError:
|
738 | |
return
|
|
908 |
return diff_found
|
739 | 909 |
|
740 | 910 |
if head != 0:
|
741 | 911 |
lines_a = lines_a[:head]
|
742 | 912 |
lines_b = lines_b[:head]
|
743 | 913 |
|
744 | 914 |
if options.matcher:
|
745 | |
lines_a = [line_a for line_a in lines_a if
|
746 | |
not re.search(options.matcher, line_a)]
|
747 | |
lines_b = [line_b for line_b in lines_b if
|
748 | |
not re.search(options.matcher, line_b)]
|
|
915 |
lines_a = [
|
|
916 |
line_a
|
|
917 |
for line_a in lines_a
|
|
918 |
if not re.search(options.matcher, line_a)
|
|
919 |
]
|
|
920 |
lines_b = [
|
|
921 |
line_b
|
|
922 |
for line_b in lines_b
|
|
923 |
if not re.search(options.matcher, line_b)
|
|
924 |
]
|
|
925 |
|
|
926 |
# Determine if a difference has been detected
|
|
927 |
diff_found = len(lines_a) or len(lines_b) or not cmp_perms(options, a, b)
|
749 | 928 |
|
750 | 929 |
if options.no_bold:
|
751 | 930 |
for key in color_mapping:
|
|
759 | 938 |
if category not in color_mapping:
|
760 | 939 |
print(
|
761 | 940 |
"Invalid category '%s' in '%s'. Valid categories are: %s."
|
762 | |
% (category, command_for_errors,
|
763 | |
", ".join(sorted(color_mapping.keys()))))
|
764 | |
sys.exit()
|
|
941 |
% (
|
|
942 |
category,
|
|
943 |
command_for_errors,
|
|
944 |
", ".join(sorted(color_mapping.keys())),
|
|
945 |
)
|
|
946 |
)
|
|
947 |
sys.exit(EXIT_CODE_ERROR)
|
765 | 948 |
|
766 | 949 |
if color not in color_codes:
|
767 | |
print("Invalid color '%s' in '%s'. Valid colors are: %s."
|
768 | |
% (color, command_for_errors,
|
769 | |
", ".join([raw_colorize(x, x)
|
770 | |
for x in sorted(color_codes.keys())])))
|
771 | |
sys.exit()
|
|
950 |
print(
|
|
951 |
"Invalid color '%s' in '%s'. Valid colors are: %s."
|
|
952 |
% (
|
|
953 |
color,
|
|
954 |
command_for_errors,
|
|
955 |
", ".join(
|
|
956 |
[
|
|
957 |
raw_colorize(x, x)
|
|
958 |
for x in sorted(color_codes.keys())
|
|
959 |
]
|
|
960 |
),
|
|
961 |
)
|
|
962 |
)
|
|
963 |
sys.exit(EXIT_CODE_ERROR)
|
772 | 964 |
|
773 | 965 |
color_mapping[category] = color
|
774 | 966 |
|
775 | |
cd = ConsoleDiff(cols=int(options.cols),
|
776 | |
show_all_spaces=options.show_all_spaces,
|
777 | |
highlight=options.highlight,
|
778 | |
line_numbers=options.line_numbers,
|
779 | |
tabsize=int(options.tabsize),
|
780 | |
truncate=options.truncate,
|
781 | |
strip_trailing_cr=options.strip_trailing_cr)
|
|
967 |
if options.permissions:
|
|
968 |
mode_a = os.lstat(a).st_mode
|
|
969 |
mode_b = os.lstat(b).st_mode
|
|
970 |
else:
|
|
971 |
mode_a = None
|
|
972 |
mode_b = None
|
|
973 |
|
|
974 |
cd = ConsoleDiff(
|
|
975 |
cols=int(options.cols),
|
|
976 |
show_all_spaces=options.show_all_spaces,
|
|
977 |
highlight=options.highlight,
|
|
978 |
line_numbers=options.line_numbers,
|
|
979 |
tabsize=int(options.tabsize),
|
|
980 |
truncate=options.truncate,
|
|
981 |
strip_trailing_cr=options.strip_trailing_cr,
|
|
982 |
)
|
782 | 983 |
for line in cd.make_table(
|
783 | |
lines_a, lines_b, headers[0], headers[1],
|
784 | |
context=(not options.whole_file),
|
785 | |
numlines=int(options.unified)):
|
|
984 |
lines_a,
|
|
985 |
lines_b,
|
|
986 |
headers[0],
|
|
987 |
headers[1],
|
|
988 |
mode_a,
|
|
989 |
mode_b,
|
|
990 |
context=(not options.whole_file),
|
|
991 |
numlines=int(options.unified),
|
|
992 |
):
|
786 | 993 |
codec_print(line, options)
|
787 | 994 |
sys.stdout.flush()
|
788 | 995 |
|
|
996 |
return diff_found
|
|
997 |
|
789 | 998 |
|
790 | 999 |
if __name__ == "__main__":
|
791 | 1000 |
start()
|