您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. #!/usr/bin/env python
  2. """ icdiff.py
  3. Author: Jeff Kaufman, derived from difflib.HtmlDiff
  4. License: This code is usable under the same open terms as the rest of
  5. python. See: http://www.python.org/psf/license/
  6. """
  7. import os
  8. import sys
  9. import errno
  10. import difflib
  11. import optparse
  12. import re
  13. import filecmp
  14. import unicodedata
  15. color_codes = {
  16. "red": '\033[0;31m',
  17. "green": '\033[0;32m',
  18. "yellow": '\033[0;33m',
  19. "blue": '\033[0;34m',
  20. "magenta": '\033[0;35m',
  21. "cyan": '\033[0;36m',
  22. "none": '\033[m',
  23. "red_bold": '\033[1;31m',
  24. "green_bold": '\033[1;32m',
  25. "yellow_bold": '\033[1;33m',
  26. "blue_bold": '\033[1;34m',
  27. "magenta_bold": '\033[1;35m',
  28. "cyan_bold": '\033[1;36m',
  29. }
  30. class ConsoleDiff(object):
  31. """Console colored side by side comparison with change highlights.
  32. Based on difflib.HtmlDiff
  33. This class can be used to create a text-mode table showing a side
  34. by side, line by line comparison of text with inter-line and
  35. intra-line change highlights in ansi color escape sequences as
  36. intra-line change highlights in ansi color escape sequences as
  37. read by xterm. The table can be generated in either full or
  38. contextual difference mode.
  39. To generate the table, call make_table.
  40. Usage is the almost the same as HtmlDiff except only make_table is
  41. implemented and the file can be invoked on the command line.
  42. Run::
  43. python icdiff.py --help
  44. for command line usage information.
  45. """
  46. def __init__(self, tabsize=8, wrapcolumn=None, linejunk=None,
  47. charjunk=difflib.IS_CHARACTER_JUNK, cols=80,
  48. line_numbers=False,
  49. show_all_spaces=False,
  50. highlight=False,
  51. no_bold=False):
  52. """ConsoleDiff instance initializer
  53. Arguments:
  54. tabsize -- tab stop spacing, defaults to 8.
  55. wrapcolumn -- column number where lines are broken and wrapped,
  56. defaults to None where lines are not wrapped.
  57. linejunk, charjunk -- keyword arguments passed into ndiff() (used by
  58. ConsoleDiff() to generate the side by side differences). See
  59. ndiff() documentation for argument default values and descriptions.
  60. """
  61. self._tabsize = tabsize
  62. self.line_numbers = line_numbers
  63. self.cols = cols
  64. self.show_all_spaces = show_all_spaces
  65. self.highlight = highlight
  66. self.no_bold = no_bold
  67. if wrapcolumn is None:
  68. if not line_numbers:
  69. wrapcolumn = self.cols // 2 - 2
  70. else:
  71. wrapcolumn = self.cols // 2 - 10
  72. self._wrapcolumn = wrapcolumn
  73. self._linejunk = linejunk
  74. self._charjunk = charjunk
  75. def _tab_newline_replace(self, fromlines, tolines):
  76. """Returns from/to line lists with tabs expanded and newlines removed.
  77. Instead of tab characters being replaced by the number of spaces
  78. needed to fill in to the next tab stop, this function will fill
  79. the space with tab characters. This is done so that the difference
  80. algorithms can identify changes in a file when tabs are replaced by
  81. spaces and vice versa. At the end of the table generation, the tab
  82. characters will be replaced with a space.
  83. """
  84. def expand_tabs(line):
  85. # hide real spaces
  86. line = line.replace(' ', '\0')
  87. # expand tabs into spaces
  88. line = line.expandtabs(self._tabsize)
  89. # relace spaces from expanded tabs back into tab characters
  90. # (we'll replace them with markup after we do differencing)
  91. line = line.replace(' ', '\t')
  92. return line.replace('\0', ' ').rstrip('\n')
  93. fromlines = [expand_tabs(line) for line in fromlines]
  94. tolines = [expand_tabs(line) for line in tolines]
  95. return fromlines, tolines
  96. def _display_len(self, s):
  97. # Handle wide characters like chinese.
  98. def width(c):
  99. if type(c) == type(u"") and unicodedata.east_asian_width(c) == 'W':
  100. return 2
  101. return 1
  102. return sum(width(c) for c in s)
  103. def _split_line(self, data_list, line_num, text):
  104. """Builds list of text lines by splitting text lines at wrap point
  105. This function will determine if the input text line needs to be
  106. wrapped (split) into separate lines. If so, the first wrap point
  107. will be determined and the first line appended to the output
  108. text line list. This function is used recursively to handle
  109. the second part of the split line to further split it.
  110. """
  111. # if blank line or context separator, just add it to the output list
  112. if not line_num:
  113. data_list.append((line_num, text))
  114. return
  115. # if line text doesn't need wrapping, just add it to the output list
  116. size = self._display_len(text)
  117. if (size <= self._wrapcolumn) or ((size - (text.count('\0') * 3)) <= self._wrapcolumn):
  118. data_list.append((line_num, text))
  119. return
  120. # scan text looking for the wrap point, keeping track if the wrap
  121. # point is inside markers
  122. i = 0
  123. n = 0
  124. mark = ''
  125. while n < self._wrapcolumn and i < size:
  126. if text[i] == '\0':
  127. i += 1
  128. mark = text[i]
  129. i += 1
  130. elif text[i] == '\1':
  131. i += 1
  132. mark = ''
  133. else:
  134. i += 1
  135. n += self._display_len(text[i])
  136. # wrap point is inside text, break it up into separate lines
  137. line1 = text[:i]
  138. line2 = text[i:]
  139. # if wrap point is inside markers, place end marker at end of first
  140. # line and start marker at beginning of second line because each
  141. # line will have its own table tag markup around it.
  142. if mark:
  143. line1 = line1 + '\1'
  144. line2 = '\0' + mark + line2
  145. # tack on first line onto the output list
  146. data_list.append((line_num, line1))
  147. # use this routine again to wrap the remaining text
  148. self._split_line(data_list, '>', line2)
  149. def _line_wrapper(self, diffs):
  150. """Returns iterator that splits (wraps) mdiff text lines"""
  151. # pull from/to data and flags from mdiff iterator
  152. for fromdata, todata, flag in diffs:
  153. # check for context separators and pass them through
  154. if flag is None:
  155. yield fromdata, todata, flag
  156. continue
  157. (fromline, fromtext), (toline, totext) = fromdata, todata
  158. # for each from/to line split it at the wrap column to form
  159. # list of text lines.
  160. fromlist, tolist = [], []
  161. self._split_line(fromlist, fromline, fromtext)
  162. self._split_line(tolist, toline, totext)
  163. # yield from/to line in pairs inserting blank lines as
  164. # necessary when one side has more wrapped lines
  165. while fromlist or tolist:
  166. if fromlist:
  167. fromdata = fromlist.pop(0)
  168. else:
  169. fromdata = ('', ' ')
  170. if tolist:
  171. todata = tolist.pop(0)
  172. else:
  173. todata = ('', ' ')
  174. yield fromdata, todata, flag
  175. def _collect_lines(self, diffs):
  176. """Collects mdiff output into separate lists
  177. Before storing the mdiff from/to data into a list, it is converted
  178. into a single line of text with console markup.
  179. """
  180. fromlist, tolist, flaglist = [], [], []
  181. # pull from/to data and flags from mdiff style iterator
  182. for fromdata, todata, flag in diffs:
  183. try:
  184. # store HTML markup of the lines into the lists
  185. fromlist.append(self._format_line(0, flag, *fromdata))
  186. tolist.append(self._format_line(1, flag, *todata))
  187. except TypeError:
  188. # exceptions occur for lines where context separators go
  189. fromlist.append(None)
  190. tolist.append(None)
  191. flaglist.append(flag)
  192. return fromlist, tolist, flaglist
  193. def _format_line(self, side, flag, linenum, text):
  194. """Returns HTML markup of "from" / "to" text lines
  195. side -- 0 or 1 indicating "from" or "to" text
  196. flag -- indicates if difference on line
  197. linenum -- line number (used for line number column)
  198. text -- line text to be marked up
  199. """
  200. try:
  201. lid = '%d' % linenum
  202. except TypeError:
  203. # handle blank lines where linenum is '>' or ''
  204. lid = ''
  205. text = text.rstrip()
  206. if not self.line_numbers:
  207. return text
  208. return '%s %s' % (self._rpad(lid, 8), text)
  209. def _real_len(self, s):
  210. l = 0
  211. in_esc = False
  212. prev = ' '
  213. for c in s.replace('\0+', "").replace('\0-', "").replace('\0^', "").replace('\1', "").replace('\t', ' '):
  214. if in_esc:
  215. if c == "m":
  216. in_esc = False
  217. else:
  218. if c == "[" and prev == "\033":
  219. in_esc = True
  220. l -= 1 # we counted prev when we shouldn't have
  221. else:
  222. l += self._display_len(c)
  223. prev = c
  224. #print("len '%s' is %d." % (s, l))
  225. return l
  226. def _rpad(self, s, field_width):
  227. return self._pad(s, field_width) + s
  228. def _pad(self, s, field_width):
  229. return " " * (field_width - self._real_len(s))
  230. def _lpad(self, s, field_width):
  231. target = s + self._pad(s, field_width)
  232. #if self._real_len(target) != field_width:
  233. # print("Warning: bad line %r is not of length %d" % (target, field_width))
  234. return target
  235. def _convert_flags(self, fromlist, tolist, flaglist, context, numlines):
  236. """Makes list of "next" links"""
  237. # all anchor names will be generated using the unique "to" prefix
  238. # process change flags, generating middle column of next anchors/links
  239. next_id = [''] * len(flaglist)
  240. next_href = [''] * len(flaglist)
  241. num_chg, in_change = 0, False
  242. last = 0
  243. toprefix = ''
  244. for i, flag in enumerate(flaglist):
  245. if flag:
  246. if not in_change:
  247. in_change = True
  248. last = i
  249. # at the beginning of a change, drop an anchor a few lines
  250. # (the context lines) before the change for the previous
  251. # link
  252. i = max([0, i - numlines])
  253. next_id[i] = ' id="difflib_chg_%s_%d"' % (toprefix, num_chg)
  254. # at the beginning of a change, drop a link to the next
  255. # change
  256. num_chg += 1
  257. next_href[last] = '<a href="#difflib_chg_%s_%d">n</a>' % (
  258. toprefix, num_chg)
  259. else:
  260. in_change = False
  261. # check for cases where there is no content to avoid exceptions
  262. if not flaglist:
  263. flaglist = [False]
  264. next_id = ['']
  265. next_href = ['']
  266. last = 0
  267. if context:
  268. fromlist = ['No Differences Found']
  269. tolist = fromlist
  270. else:
  271. fromlist = tolist = ['Empty File']
  272. # if not a change on first line, drop a link
  273. if not flaglist[0]:
  274. next_href[0] = '<a href="#difflib_chg_%s_0">f</a>' % toprefix
  275. # redo the last link to link to the top
  276. next_href[last] = '<a href="#difflib_chg_%s_top">t</a>' % (toprefix)
  277. return fromlist, tolist, flaglist, next_href, next_id
  278. def make_table(self, fromlines, tolines, fromdesc='', todesc='', context=False,
  279. numlines=5):
  280. """Returns table of side by side comparison with change highlights
  281. Arguments:
  282. fromlines -- list of "from" lines
  283. tolines -- list of "to" lines
  284. fromdesc -- "from" file column header string
  285. todesc -- "to" file column header string
  286. context -- set to True for contextual differences (defaults to False
  287. which shows full differences).
  288. numlines -- number of context lines. When context is set True,
  289. controls number of lines displayed before and after the change.
  290. When context is False, controls the number of lines to place
  291. the "next" link anchors before the next change (so click of
  292. "next" link jumps to just before the change).
  293. """
  294. # change tabs to spaces before it gets more difficult after we insert
  295. # markkup
  296. fromlines, tolines = self._tab_newline_replace(fromlines, tolines)
  297. # create diffs iterator which generates side by side from/to data
  298. if context:
  299. context_lines = numlines
  300. else:
  301. context_lines = None
  302. diffs = difflib._mdiff(fromlines, tolines, context_lines, linejunk=self._linejunk,
  303. charjunk=self._charjunk)
  304. # set up iterator to wrap lines that exceed desired width
  305. if self._wrapcolumn:
  306. diffs = self._line_wrapper(diffs)
  307. # collect up from/to lines and flags into lists (also format the lines)
  308. fromlist, tolist, flaglist = self._collect_lines(diffs)
  309. # process change flags, generating middle column of next anchors/links
  310. fromlist, tolist, flaglist, next_href, next_id = self._convert_flags(
  311. fromlist, tolist, flaglist, context, numlines)
  312. s = []
  313. if fromdesc or todesc:
  314. s.append((simple_colorize(fromdesc, "blue"),
  315. simple_colorize(todesc, "blue")))
  316. for i in range(len(flaglist)):
  317. if flaglist[i] is None:
  318. # mdiff yields None on separator lines; skip the bogus ones
  319. # generated for the first line
  320. if i > 0:
  321. s.append((simple_colorize('---', "blue"),
  322. simple_colorize('---', "blue")))
  323. else:
  324. s.append((fromlist[i], tolist[i]))
  325. table_lines = []
  326. for sides in s:
  327. line = []
  328. for side in sides:
  329. line.append(self._lpad(side, self.cols // 2 - 1))
  330. table_lines.append(" ".join(line))
  331. table_line_string = "\n".join(table_lines)
  332. colorized_table_line_string = self.colorize(table_line_string)
  333. return colorized_table_line_string
  334. def colorize(self, s):
  335. def background(color):
  336. return color.replace("\033[1;", "\033[7;")
  337. if self.no_bold:
  338. C_ADD = color_codes["green"]
  339. C_SUB = color_codes["red"]
  340. C_CHG = color_codes["yellow"]
  341. else:
  342. C_ADD = color_codes["green_bold"]
  343. C_SUB = color_codes["red_bold"]
  344. C_CHG = color_codes["yellow_bold"]
  345. if self.highlight:
  346. C_ADD, C_SUB, C_CHG = background(C_ADD), background(C_SUB), background(C_CHG)
  347. C_NONE = color_codes["none"]
  348. colors = (C_ADD, C_SUB, C_CHG, C_NONE)
  349. s = s.replace('\0+', C_ADD).replace('\0-', C_SUB).replace('\0^', C_CHG).replace('\1', C_NONE).replace('\t', ' ')
  350. if self.highlight:
  351. return s
  352. if not self.show_all_spaces:
  353. # If there's a change consisting entirely of whitespace, don't color it.
  354. return re.sub("\033\\[[01];3([123])m(\\s+)(\033\\[)", "\033[7;3\\1m\\2\\3", s)
  355. def will_see_coloredspace(i, s):
  356. while i < len(s) and s[i].isspace():
  357. i += 1
  358. if i < len(s) and s[i] == '\033':
  359. return False
  360. return True
  361. n_s = []
  362. in_color = False
  363. seen_coloredspace = False
  364. for i, c in enumerate(s):
  365. if len(n_s) > 6 and n_s[-1] == "m":
  366. ns_end = "".join(n_s[-7:])
  367. for color in colors:
  368. if ns_end.endswith(color):
  369. if color != in_color:
  370. seen_coloredspace = False
  371. in_color = color
  372. if ns_end.endswith(C_NONE):
  373. in_color = False
  374. if c.isspace() and in_color and (self.show_all_spaces or not (seen_coloredspace or will_see_coloredspace(i, s))):
  375. n_s.extend([C_NONE, background(in_color), c, C_NONE, in_color])
  376. else:
  377. if in_color:
  378. seen_coloredspace = True
  379. n_s.append(c)
  380. joined = "".join(n_s)
  381. return joined
  382. def simple_colorize(s, chosen_color):
  383. return "%s%s%s" % (color_codes[chosen_color], s, color_codes["none"])
  384. def start():
  385. # If you change any of these, also update README.
  386. parser = optparse.OptionParser(usage="usage: %prog [options] left_file right_file",
  387. description="Show differences between files in a two column view.")
  388. parser.add_option("--cols", default=None,
  389. help="specify the width of the screen. Autodetection is Linux only")
  390. parser.add_option("--head", default=0,
  391. help="consider only the first N lines of each file")
  392. parser.add_option("--highlight", default=False,
  393. action="store_true",
  394. help="color by changing the background color instead of the foreground color. Very fast, ugly, displays all changes")
  395. parser.add_option("--line-numbers", default=False,
  396. action="store_true",
  397. help="generate output with line numbers")
  398. parser.add_option("--no-bold", default=False,
  399. action="store_true",
  400. help="use non-bold colors; recommended for with solarized")
  401. parser.add_option("--no-headers", default=False,
  402. action="store_true",
  403. help="don't label the left and right sides with their file names")
  404. parser.add_option("--numlines", default=5,
  405. help="how many lines of context to print; can't be combined with --whole-file")
  406. parser.add_option("--recursive", default=False,
  407. action="store_true",
  408. help="recursively compare subdirectories")
  409. parser.add_option("--show-all-spaces", default=False,
  410. action="store_true",
  411. help="color all non-matching whitespace including that which is not needed for drawing the eye to changes. Slow, ugly, displays all changes")
  412. parser.add_option("--version", default=False,
  413. action="store_true",
  414. help="print version and exit")
  415. parser.add_option("--whole-file", default=False,
  416. action="store_true",
  417. help="show the whole file instead of just changed lines and context")
  418. (options, args) = parser.parse_args()
  419. if options.version:
  420. print("icdiff version 1.2.1")
  421. sys.exit()
  422. if len(args) != 2:
  423. parser.print_help()
  424. sys.exit()
  425. a, b = args
  426. if not options.cols:
  427. def ioctl_GWINSZ(fd):
  428. try:
  429. import fcntl, termios, struct
  430. cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
  431. except Exception:
  432. return None
  433. return cr
  434. cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
  435. if cr:
  436. options.cols = cr[1]
  437. else:
  438. options.cols = 80
  439. if options.recursive:
  440. diff_recursively(options, a, b)
  441. else:
  442. diff_files(options, a, b)
  443. def diff_recursively(options, a, b):
  444. def print_meta(s):
  445. print(simple_colorize(s, "magenta"))
  446. if os.path.isfile(a) and os.path.isfile(b):
  447. if not filecmp.cmp(a, b):
  448. diff_files(options, a, b)
  449. elif os.path.isdir(a) and os.path.isdir(b):
  450. a_contents = set(os.listdir(a))
  451. b_contents = set(os.listdir(b))
  452. for child in sorted(a_contents.union(b_contents)):
  453. if child not in b_contents:
  454. print_meta("Only in %s: %s" % (a, child))
  455. elif child not in a_contents:
  456. print_meta("Only in %s: %s" % (b, child))
  457. else:
  458. diff_recursively(options,
  459. os.path.join(a, child),
  460. os.path.join(b, child))
  461. elif os.path.isdir(a) and os.path.isfile(b):
  462. print_meta("File %s is a directory while %s is a file" % (a, b))
  463. elif os.path.isfile(a) and os.path.isdir(b):
  464. print_meta("File %s is a file while %s is a directory" % (a, b))
  465. def diff_files(options, a, b):
  466. headers = a, b
  467. if options.no_headers:
  468. headers = None, None
  469. head = int(options.head)
  470. for x in [a, b]:
  471. if os.path.isdir(x):
  472. sys.stderr.write("error: %s is a directory; did you mean to pass --recursive?\n" % x)
  473. sys.exit(1)
  474. lines_a = open(a, "U").readlines()
  475. lines_b = open(b, "U").readlines()
  476. if head != 0:
  477. lines_a = lines_a[:head]
  478. lines_b = lines_b[:head]
  479. print(ConsoleDiff(cols=int(options.cols),
  480. show_all_spaces=options.show_all_spaces,
  481. highlight=options.highlight,
  482. no_bold=options.no_bold,
  483. line_numbers=options.line_numbers).make_table(
  484. lines_a, lines_b, headers[0], headers[1], context=(not options.whole_file), numlines=int(options.numlines)))
  485. sys.stdout.flush()
  486. if __name__ == "__main__":
  487. try:
  488. start()
  489. except KeyboardInterrupt:
  490. pass
  491. except IOError as e:
  492. if e.errno == errno.EPIPE:
  493. pass
  494. else:
  495. raise