Codebase list icdiff / 2b706e3
New upstream version 2.0.5 Sascha Steinbiss 2 years ago
12 changed file(s) with 570 addition(s) and 278 deletion(s). Raw diff Collapse all Expand all
0 2.0.5
1 Set process exit code to indicate differences #195
2 Support -P/--permissions option #197
3
04 2.0.4
15 Include LICENSE in package
26
149149 * `git push`
150150 * `git tag release-${version}`
151151 * `git push origin release-${version}`
152 * `prepare-release.sh ${prev-version} ${version}`
153 * `python3 -m twine upload icdiff-${version}.tar.gz --user "__token__" --password "$TOKEN"`
152154
153155 ## License
154156
+435
-226
icdiff less more
1313 with changes to provide console output instead of html output. """
1414
1515 import os
16 import stat
1617 import sys
1718 import errno
1819 import difflib
2223 import unicodedata
2324 import codecs
2425
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
2632
2733 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",
4551 }
4652
4753
5157 "change": "yellow_bold",
5258 "separator": "blue",
5359 "description": "blue",
60 "permissions": "yellow",
5461 "meta": "magenta",
5562 "line-numbers": "white",
5663 }
8188
8289 """
8390
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 ):
91104 """ConsoleDiff instance initializer
92105
93106 Arguments:
127140 spaces and vice versa. At the end of the table generation, the tab
128141 characters will be replaced with a space.
129142 """
143
130144 def expand_tabs(line):
131145 # hide real spaces
132 line = line.replace(' ', '\0')
146 line = line.replace(" ", "\0")
133147 # expand tabs into spaces
134148 line = line.expandtabs(self._tabsize)
135149 # replace spaces from expanded tabs back into tab characters
136150 # (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
139154 fromlines = [expand_tabs(line) for line in fromlines]
140155 tolines = [expand_tabs(line) for line in tolines]
141156 return fromlines, tolines
142157
143158 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]
147161 return lines
148162
149163 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)
153166
154167 def _display_len(self, s):
155168 # Handle wide characters like Chinese.
156169 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 ):
159174 return 2
160 elif c == '\r':
175 elif c == "\r":
161176 return 2
162177 return 1
163178
182197
183198 # if line text doesn't need wrapping, just add it to the output
184199 # 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 ):
187204 data_list.append((line_num, text))
188205 return
189206
191208 # point is inside markers
192209 i = 0
193210 n = 0
194 mark = ''
211 mark = ""
195212 while n < self._wrapcolumn and i < len(text):
196 if text[i] == '\0':
213 if text[i] == "\0":
197214 i += 1
198215 mark = text[i]
199216 i += 1
200 elif text[i] == '\1':
217 elif text[i] == "\1":
201218 i += 1
202 mark = ''
219 mark = ""
203220 else:
204221 n += self._display_len(text[i])
205222 i += 1
212229 # line and start marker at beginning of second line because each
213230 # line will have its own table tag markup around it.
214231 if mark:
215 line1 = line1 + '\1'
216 line2 = '\0' + mark + line2
232 line1 = line1 + "\1"
233 line2 = "\0" + mark + line2
217234
218235 # tack on first line onto the output list
219236 data_list.append((line_num, line1))
222239 # unless truncate is set
223240 if self.truncate:
224241 return
225 line_num = '>'
242 line_num = ">"
226243 text = line2
227244
228245 def _line_wrapper(self, diffs):
246263 if fromlist:
247264 fromdata = fromlist.pop(0)
248265 else:
249 fromdata = ('', ' ')
266 fromdata = ("", " ")
250267 if tolist:
251268 todata = tolist.pop(0)
252269 else:
253 todata = ('', ' ')
270 todata = ("", " ")
254271 yield fromdata, todata, flag
255272
256273 def _collect_lines(self, diffs):
265282 if (fromdata, todata, flag) == (None, None, None):
266283 yield None
267284 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 )
270289
271290 def _format_line(self, linenum, text):
272291 text = text.rstrip()
276295
277296 def _add_line_numbers(self, linenum, text):
278297 try:
279 lid = '%d' % linenum
298 lid = "%d" % linenum
280299 except TypeError:
281300 # handle blank lines where linenum is '>' or ''
282 lid = ''
301 lid = ""
283302 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 )
286307
287308 def _real_len(self, s):
288309 s_len = 0
289310 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 ):
296315 if in_esc:
297316 if c == "m":
298317 in_esc = False
315334 def _lpad(self, s, field_width):
316335 return s + self._pad(s, field_width)
317336
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 ):
320348 """Generates table of side by side comparison with change highlights
321349
322350 Arguments:
324352 tolines -- list of "to" lines
325353 fromdesc -- "from" file column header string
326354 todesc -- "to" file column header string
355 fromperms -- "from" file permissions
356 toperms -- "to" file permissions
327357 context -- set to True for contextual differences (defaults to False
328358 which shows full differences).
329359 numlines -- number of context lines. When context is set True,
342372 fromlines, tolines = self._tab_newline_replace(fromlines, tolines)
343373
344374 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 ):
346377 fromlines = self._strip_trailing_cr(fromlines)
347378 tolines = self._strip_trailing_cr(tolines)
348379
349380 # 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 )
353388
354389 # set up iterator to wrap lines that exceed desired width
355390 if self._wrapcolumn:
356391 diffs = self._line_wrapper(diffs)
357392 diffs = self._collect_lines(diffs)
358393
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 ):
360397 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):
365406 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 )
368422
369423 for i, line in enumerate(diffs):
370424 if line is None:
371425 # mdiff yields None on separator lines; skip the bogus ones
372426 # generated for the first line
373427 if i > 0:
374 yield (simple_colorize('---', "separator"),
375 simple_colorize('---', "separator"))
428 yield (
429 simple_colorize("---", "separator"),
430 simple_colorize("---", "separator"),
431 )
376432 else:
377433 yield line
378434
379435 def colorize(self, s):
380436 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 )
383440
384441 C_ADD = color_codes[color_mapping["add"]]
385442 C_SUB = color_codes[color_mapping["subtract"]]
386443 C_CHG = color_codes[color_mapping["change"]]
387444
388445 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 )
392451
393452 C_NONE = color_codes["none"]
394453 colors = (C_ADD, C_SUB, C_CHG, C_NONE)
395454
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 )
402466
403467 if self.highlight:
404468 return s
406470 if not self.show_all_spaces:
407471 # If there's a change consisting entirely of whitespace,
408472 # 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 )
411478
412479 def will_see_coloredspace(i, s):
413480 while i < len(s) and s[i].isspace():
414481 i += 1
415 if i < len(s) and s[i] == '\033':
482 if i < len(s) and s[i] == "\033":
416483 return False
417484 return True
418485
430497 if ns_end.endswith(C_NONE):
431498 in_color = False
432499
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 ):
436508 n_s.extend([C_NONE, background(in_color), c, C_NONE, in_color])
437509 else:
438510 if in_color:
473545
474546 def create_option_parser():
475547 # 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 )
562708 return parser
563709
564710
565711 def set_cols_option(options):
566 if os.name == 'nt':
712 if os.name == "nt":
567713 try:
568714 import struct
569715 from ctypes import windll, create_string_buffer
579725 pass
580726
581727 else:
728
582729 def ioctl_GWINSZ(fd):
583730 try:
584731 import fcntl
585732 import termios
586733 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 )
590738 except Exception:
591739 return None
592740 return cr
741
593742 cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
594743 if cr and cr[1] > 0:
595744 options.cols = cr[1]
600749 def validate_has_two_arguments(parser, args):
601750 if len(args) != 2:
602751 parser.print_help()
603 sys.exit()
752 sys.exit(EXIT_CODE_DIFF)
604753
605754
606755 def start():
756 diffs_found = False
607757 parser = create_option_parser()
608758 options, args = parser.parse_args()
609759 validate_has_two_arguments(parser, args)
610760 if not options.cols:
611761 set_cols_option(options)
612762 try:
613 diff(options, *args)
763 diffs_found = diff(options, *args)
614764 except KeyboardInterrupt:
615765 pass
616766 except IOError as e:
625775 # See: https://stackoverflow.com/questions/26692284/...
626776 # ...how-to-prevent-brokenpipeerror-when-doing-a-flush-in-python
627777 sys.stderr.close()
778 sys.exit(EXIT_CODE_DIFF if diffs_found else EXIT_CODE_SUCCESS)
628779
629780
630781 def codec_print(s, options):
635786 sys.stdout.write(s.encode(options.output_encoding))
636787
637788
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
638795 def diff(options, a, b):
639796 def print_meta(s):
640797 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
641801
642802 # Don't use os.path.isfile; it returns False for file-like entities like
643803 # bash's process substitution (/dev/fd/N).
646806
647807 if is_a_file and is_b_file:
648808 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)
651813 elif options.report_identical_files:
652814 print("Files %s and %s are identical." % (a, b))
653815 except OSError as e:
654816 if e.errno == errno.ENOENT:
655817 print_meta("error: file '%s' was not found" % e.filename)
818 sys.exit(EXIT_CODE_ERROR)
656819 else:
657 raise(e)
820 raise (e)
658821
659822 elif not is_a_file and not is_b_file:
660823 a_contents = set(os.listdir(a))
666829 elif child not in a_contents:
667830 print_meta("Only in %s: %s" % (b, child))
668831 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 )
672835 elif not is_a_file and is_b_file:
673836 print_meta("File %s is a directory while %s is a file" % (a, b))
837 diffs_found = True
674838
675839 elif is_a_file and not is_b_file:
676840 print_meta("File %s is a file while %s is a directory" % (a, b))
841 diffs_found = True
842
843 return diffs_found
677844
678845
679846 def read_file(fname, options):
682849 return inf.readlines()
683850 except UnicodeDecodeError as e:
684851 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 )
687856 raise
688857 except LookupError:
689858 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)
693862
694863
695864 def format_label(path, label="{path}"):
697866
698867 Example:
699868 For file `/foo/bar.py` and label "Yours: {basename}" -
700 The ouptut is "Yours: bar.py"
869 The output is "Yours: bar.py"
701870 """
702871 return label.format(path=path, basename=os.path.basename(path))
703872
704873
705874 def diff_files(options, a, b):
875 diff_found = False
706876 if options.is_git_diff is True:
707877 # Use $BASE as label when displaying git-diff result
708878 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)]
713880 else:
714881 if options.labels:
715882 if len(options.labels) == 2:
718885 format_label(b, options.labels[1]),
719886 ]
720887 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)
724894 else:
725895 headers = a, b
726896 if options.no_headers:
735905 lines_a = read_file(a, options)
736906 lines_b = read_file(b, options)
737907 except UnicodeDecodeError:
738 return
908 return diff_found
739909
740910 if head != 0:
741911 lines_a = lines_a[:head]
742912 lines_b = lines_b[:head]
743913
744914 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)
749928
750929 if options.no_bold:
751930 for key in color_mapping:
759938 if category not in color_mapping:
760939 print(
761940 "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)
765948
766949 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)
772964
773965 color_mapping[category] = color
774966
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 )
782983 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 ):
786993 codec_print(line, options)
787994 sys.stdout.flush()
788995
996 return diff_found
997
789998
790999 if __name__ == "__main__":
7911000 start()
0 # I don't know what I'm doing, I just want to get a release out.
1
2 if [[ "$#" != 2 ]]; then
3 echo "usage: $0 <prev-version> <version>"
4 echo "ex: $0 2.0.3 2.0.4"
5 exit 1
6 fi
7
8 PREV="$1"
9 VERSION="$2"
10
11 GITDIR="$(dirname "$0")"
12 PREVDIR="icdiff-$PREV"
13 OUTDIR="icdiff-$VERSION"
14 OUTTAR="$OUTDIR.tar.gz"
15
16 rm -r "$OUTDIR" || true
17 cp -r "$PREVDIR" "$OUTDIR"
18
19 for x in icdiff.py README.md git-icdiff LICENSE; do
20 cp "$GITDIR/$x" "$OUTDIR/$x"
21 done
22
23 sed "s/Version: $PREV/Version: $VERSION/" "$OUTDIR/PKG-INFO" \
24 > "$OUTDIR/PKG-INFO.2"
25 mv "$OUTDIR/PKG-INFO.2" "$OUTDIR/PKG-INFO"
26 cp "$OUTDIR/PKG-INFO" "$OUTDIR/icdiff.egg-info/"
27 tar -cvzf "$OUTTAR" "$OUTDIR/"*
11 mccabe==0.3
22 pep8==1.5.7
33 pyflakes==0.8.1
4 black==22.3.0
55 name="icdiff",
66 version=__version__,
77 url="http://www.jefftk.com/icdiff",
8 project_urls={
9 "Source": "https://github.com/jeffkaufman/icdiff",
10 },
811 author="Jeff Kaufman",
912 author_email="jeff@jefftk.com",
1013 description="improved colored diff",
4343 }
4444
4545 function check_gold() {
46 local gold=tests/$1
46 local error_code
47 local expect=$1
48 local gold=tests/$2
49 shift
4750 shift
4851
4952 if [ $TEST_NAME != "all" -a $TEST_NAME != $gold ]; then
5356 echo " check_gold $gold matches $@"
5457 local tmp=/tmp/icdiff.output
5558 $INVOCATION "$@" &> $tmp
59 error_code=$?
5660
5761 if $REGOLD; then
5862 if [ -e $gold ] && diff $tmp $gold > /dev/null; then
7882 cat $gold
7983 fail
8084 fi
85
86 if [[ $error_code != $expect ]]; then
87 echo "Got error code: $error_code"
88 echo "Expected error code: $expect"
89 fail
90 fi
8191 }
8292
8393 FIRST_TIME_CHECK_GIT_DIFF=true
104114 fi
105115 }
106116
107 check_gold gold-recursive.txt --recursive tests/{a,b} --cols=80
108 check_gold gold-exclude.txt --exclude-lines '^#| pad' tests/input-4-cr.txt tests/input-4-partial-cr.txt --cols=80
109 check_gold gold-dir.txt tests/{a,b} --cols=80
110 check_gold gold-12.txt tests/input-{1,2}.txt --cols=80
111 check_gold gold-12-t.txt tests/input-{1,2}.txt --cols=80 --truncate
112 check_gold gold-3.txt tests/input-{3,3}.txt
113 check_gold gold-45.txt tests/input-{4,5}.txt --cols=80
114 check_gold gold-45-95.txt tests/input-{4,5}.txt --cols=95
115 check_gold gold-45-sas.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces
116 check_gold gold-45-h.txt tests/input-{4,5}.txt --cols=80 --highlight
117 check_gold gold-45-nb.txt tests/input-{4,5}.txt --cols=80 --no-bold
118 check_gold gold-45-sas-h.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces --highlight
119 check_gold gold-45-sas-h-nb.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces --highlight --no-bold
120 check_gold gold-45-h-nb.txt tests/input-{4,5}.txt --cols=80 --highlight --no-bold
121 check_gold gold-45-ln.txt tests/input-{4,5}.txt --cols=80 --line-numbers
122 check_gold gold-45-ln-color.txt tests/input-{4,5}.txt --cols=80 --line-numbers --color-map='line-numbers:cyan'
123 check_gold gold-45-nh.txt tests/input-{4,5}.txt --cols=80 --no-headers
124 check_gold gold-45-h3.txt tests/input-{4,5}.txt --cols=80 --head=3
125 check_gold gold-45-l.txt tests/input-{4,5}.txt --cols=80 -L left
126 check_gold gold-45-lr.txt tests/input-{4,5}.txt --cols=80 -L left -L right
127 check_gold gold-45-lbrb.txt tests/input-{4,5}.txt --cols=80 -L "L {basename}" -L "R {basename}"
128 check_gold gold-45-pipe.txt tests/input-4.txt <(cat tests/input-5.txt) --cols=80 --no-headers
129 check_gold gold-4dn.txt tests/input-4.txt /dev/null --cols=80 -L left -L right
130 check_gold gold-dn5.txt /dev/null tests/input-5.txt --cols=80 -L left -L right
131 check_gold gold-67.txt tests/input-{6,7}.txt --cols=80
132 check_gold gold-67-wf.txt tests/input-{6,7}.txt --cols=80 --whole-file
133 check_gold gold-67-ln.txt tests/input-{6,7}.txt --cols=80 --line-numbers
134 check_gold gold-67-u3.txt tests/input-{6,7}.txt --cols=80 -U 3
135 check_gold gold-tabs-default.txt tests/input-{8,9}.txt --cols=80
136 check_gold gold-tabs-4.txt tests/input-{8,9}.txt --cols=80 --tabsize=4
137 check_gold gold-file-not-found.txt tests/input-4.txt nonexistent_file
138 check_gold gold-strip-cr-off.txt tests/input-4.txt tests/input-4-cr.txt --cols=80
139 check_gold gold-strip-cr-on.txt tests/input-4.txt tests/input-4-cr.txt --cols=80 --strip-trailing-cr
140 check_gold gold-no-cr-indent tests/input-4-cr.txt tests/input-4-partial-cr.txt --cols=80
141 check_gold gold-hide-cr-if-dos tests/input-4-cr.txt tests/input-5-cr.txt --cols=80
142 check_gold gold-12-subcolors.txt tests/input-{1,2}.txt --cols=80 --color-map='change:magenta,description:cyan_bold'
143 check_gold gold-subcolors-bad-color tests/input-{1,2}.txt --cols=80 --color-map='change:mageta,description:cyan_bold'
144 check_gold gold-subcolors-bad-cat tests/input-{1,2}.txt --cols=80 --color-map='chnge:magenta,description:cyan_bold'
145 check_gold gold-subcolors-bad-fmt tests/input-{1,2}.txt --cols=80 --color-map='change:magenta:gold,description:cyan_bold'
146 check_gold gold-identical-on.txt tests/input-{1,1}.txt -s
147 check_gold gold-bad-encoding.txt tests/input-{1,2}.txt --encoding=nonexistend_encoding
117 check_gold 1 gold-recursive.txt --recursive tests/{a,b} --cols=80
118 check_gold 1 gold-exclude.txt --exclude-lines '^#| pad' tests/input-4-cr.txt tests/input-4-partial-cr.txt --cols=80
119 check_gold 0 gold-dir.txt tests/{a,b} --cols=80
120 check_gold 1 gold-12.txt tests/input-{1,2}.txt --cols=80
121 check_gold 1 gold-12-t.txt tests/input-{1,2}.txt --cols=80 --truncate
122 check_gold 0 gold-3.txt tests/input-{3,3}.txt
123 check_gold 1 gold-45.txt tests/input-{4,5}.txt --cols=80
124 check_gold 1 gold-45-95.txt tests/input-{4,5}.txt --cols=95
125 check_gold 1 gold-45-sas.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces
126 check_gold 1 gold-45-h.txt tests/input-{4,5}.txt --cols=80 --highlight
127 check_gold 1 gold-45-nb.txt tests/input-{4,5}.txt --cols=80 --no-bold
128 check_gold 1 gold-45-sas-h.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces --highlight
129 check_gold 1 gold-45-sas-h-nb.txt tests/input-{4,5}.txt --cols=80 --show-all-spaces --highlight --no-bold
130 check_gold 1 gold-45-h-nb.txt tests/input-{4,5}.txt --cols=80 --highlight --no-bold
131 check_gold 1 gold-45-ln.txt tests/input-{4,5}.txt --cols=80 --line-numbers
132 check_gold 1 gold-45-ln-color.txt tests/input-{4,5}.txt --cols=80 --line-numbers --color-map='line-numbers:cyan'
133 check_gold 1 gold-45-nh.txt tests/input-{4,5}.txt --cols=80 --no-headers
134 check_gold 1 gold-45-h3.txt tests/input-{4,5}.txt --cols=80 --head=3
135 check_gold 2 gold-45-l.txt tests/input-{4,5}.txt --cols=80 -L left
136 check_gold 1 gold-45-lr.txt tests/input-{4,5}.txt --cols=80 -L left -L right
137 check_gold 1 gold-45-lbrb.txt tests/input-{4,5}.txt --cols=80 -L "L {basename}" -L "R {basename}"
138 check_gold 1 gold-45-pipe.txt tests/input-4.txt <(cat tests/input-5.txt) --cols=80 --no-headers
139 check_gold 1 gold-4dn.txt tests/input-4.txt /dev/null --cols=80 -L left -L right
140 check_gold 1 gold-dn5.txt /dev/null tests/input-5.txt --cols=80 -L left -L right
141 check_gold 1 gold-67.txt tests/input-{6,7}.txt --cols=80
142 check_gold 1 gold-67-wf.txt tests/input-{6,7}.txt --cols=80 --whole-file
143 check_gold 1 gold-67-ln.txt tests/input-{6,7}.txt --cols=80 --line-numbers
144 check_gold 1 gold-67-u3.txt tests/input-{6,7}.txt --cols=80 -U 3
145 check_gold 1 gold-tabs-default.txt tests/input-{8,9}.txt --cols=80
146 check_gold 1 gold-tabs-4.txt tests/input-{8,9}.txt --cols=80 --tabsize=4
147 check_gold 2 gold-file-not-found.txt tests/input-4.txt nonexistent_file
148 check_gold 1 gold-strip-cr-off.txt tests/input-4.txt tests/input-4-cr.txt --cols=80
149 check_gold 1 gold-strip-cr-on.txt tests/input-4.txt tests/input-4-cr.txt --cols=80 --strip-trailing-cr
150 check_gold 1 gold-no-cr-indent tests/input-4-cr.txt tests/input-4-partial-cr.txt --cols=80
151 check_gold 1 gold-hide-cr-if-dos tests/input-4-cr.txt tests/input-5-cr.txt --cols=80
152 check_gold 1 gold-12-subcolors.txt tests/input-{1,2}.txt --cols=80 --color-map='change:magenta,description:cyan_bold'
153 check_gold 2 gold-subcolors-bad-color tests/input-{1,2}.txt --cols=80 --color-map='change:mageta,description:cyan_bold'
154 check_gold 2 gold-subcolors-bad-cat tests/input-{1,2}.txt --cols=80 --color-map='chnge:magenta,description:cyan_bold'
155 check_gold 2 gold-subcolors-bad-fmt tests/input-{1,2}.txt --cols=80 --color-map='change:magenta:gold,description:cyan_bold'
156 check_gold 0 gold-identical-on.txt tests/input-{1,1}.txt -s
157 check_gold 2 gold-bad-encoding.txt tests/input-{1,2}.txt --encoding=nonexistend_encoding
158
159 rm tests/permissions-{a,b}
160 touch tests/permissions-{a,b}
161 check_gold 0 gold-permissions-same.txt tests/permissions-{a,b} -P --cols=80
162
163 chmod 666 tests/permissions-a
164 chmod 665 tests/permissions-b
165 check_gold 1 gold-permissions-diff.txt tests/permissions-{a,b} -P --cols=80
166
167 echo "some text" >> tests/permissions-a
168 check_gold 1 gold-permissions-diff-text.txt tests/permissions-{a,b} -P --cols=80
169
170 echo -e "\04" >> tests/permissions-b
171 check_gold 1 gold-permissions-diff-binary.txt tests/permissions-{a,b} -P --cols=80
172 rm tests/permissions-{a,b}
148173
149174 if git show 4e86205629 &> /dev/null; then
150175 # We're in the repo, so test git.
166191 fail
167192 fi
168193
169 if ! command -v 'flake8' >/dev/null 2>&1; then
170 echo 'Could not find flake8. Ensure flake8 is installed and on your $PATH.'
171 if [ -z "$VIRTUAL_ENV" ]; then
172 echo 'It appears you have have forgotten to activate your virtualenv.'
173 fi
174 echo 'See README.md for details on setting up your environment.'
175 fail
176 fi
177
194 function ensure_installed() {
195 if ! command -v "$1" >/dev/null 2>&1; then
196 echo "Could not find $1."
197 echo 'Ensure it is installed and on your $PATH.'
198 if [ -z "$VIRTUAL_ENV" ]; then
199 echo 'It appears you have have forgotten to activate your virtualenv.'
200 fi
201 echo 'See README.md for details on setting up your environment.'
202 fail
203 fi
204 }
205
206 ensure_installed "black"
207 echo 'Running black formatter...'
208 if ! black icdiff --line-length 79 --check; then
209 echo ""
210 echo 'Consider running `black icdiff --line-length 79`'
211 fail
212 fi
213
214 ensure_installed "flake8"
178215 echo 'Running flake8 linter...'
179216 if ! flake8 icdiff; then
180217 fail
0 tests/permissions-a tests/permissions-b
1 -rw-rw-rw- (100666) -rw-rw-r-x (100665)
2 some text 
0 tests/permissions-a tests/permissions-b
1 -rw-rw-rw- (100666) -rw-rw-r-x (100665)
2 some text
0 tests/permissions-a tests/permissions-b
1 -rw-rw-rw- (100666) -rw-rw-r-x (100665)
0 Invalid category 'chnge' in '--color-map="chnge:magenta,description:cyan_bold"'. Valid categories are: add, change, description, line-numbers, meta, separator, subtract.
0 Invalid category 'chnge' in '--color-map="chnge:magenta,description:cyan_bold"'. Valid categories are: add, change, description, line-numbers, meta, permissions, separator, subtract.