typocheck 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/bin/icdiff +610 -0
- data/bin/typocheck +121955 -0
- data/lib/big.txt +121628 -0
- data/lib/comment_getter.rb +120 -0
- data/lib/custom.txt +0 -0
- data/lib/holmes.txt +5501 -0
- data/lib/model.rb +26 -0
- data/lib/model.rb.corrected +26 -0
- data/lib/spell_checker.rb +92 -0
- data/tools/icdiff +610 -0
- data/typocheck.gemspec +18 -0
- metadata +58 -0
data/lib/model.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Model
|
|
2
|
+
# ++
|
|
3
|
+
# Term-Frequency
|
|
4
|
+
# ++
|
|
5
|
+
attr_reader :tf
|
|
6
|
+
|
|
7
|
+
def initialize(base_text: base_text)
|
|
8
|
+
@tf = train(words(File.read(base_text)))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# ++
|
|
12
|
+
# remove specail character
|
|
13
|
+
# ++
|
|
14
|
+
def words(text)
|
|
15
|
+
text.downcase.scan(/[a-z]+/)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ++
|
|
19
|
+
# Term-Frequency count
|
|
20
|
+
# ++
|
|
21
|
+
def train(words)
|
|
22
|
+
model = Hash.new(1)
|
|
23
|
+
words.each {|f| model[f] += 1 }
|
|
24
|
+
model
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Model
|
|
2
|
+
# ++
|
|
3
|
+
# Term-Frequency
|
|
4
|
+
# ++
|
|
5
|
+
attr_reader :tf
|
|
6
|
+
|
|
7
|
+
def initialize(base_text: base_text)
|
|
8
|
+
@tf = train(words(File.read(base_text)))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# ++
|
|
12
|
+
# remove special character
|
|
13
|
+
# ++
|
|
14
|
+
def words(text)
|
|
15
|
+
text.downcase.scan(/[a-z]+/)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ++
|
|
19
|
+
# Term-Frequency count
|
|
20
|
+
# ++
|
|
21
|
+
def train(words)
|
|
22
|
+
model = Hash.new(1)
|
|
23
|
+
words.each {|f| model[f] += 1 }
|
|
24
|
+
model
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
class SpellChecker
|
|
2
|
+
def initialize(model:)
|
|
3
|
+
raise "No model given, cannot do spell checking" if model == nil
|
|
4
|
+
@letters = ('a'..'z').to_a.join
|
|
5
|
+
@model = model
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def correct_on_text(text)
|
|
9
|
+
# ++
|
|
10
|
+
# should notice that correction happend right on the origin text
|
|
11
|
+
# ++
|
|
12
|
+
correct = {}
|
|
13
|
+
text.scan(/\w+/).each do |word|
|
|
14
|
+
correct[word] = correct_on_word(word)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
correct.each_pair do |key, val|
|
|
18
|
+
text.gsub!(key, val)
|
|
19
|
+
end
|
|
20
|
+
text
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def correct_on_word(word)
|
|
24
|
+
return word if word.upcase == word
|
|
25
|
+
return word if word[0].upcase == word[0]
|
|
26
|
+
(known([word]) or known(edits1(word)) or known_edits2(word) or
|
|
27
|
+
[word]).max {|a,b| @model.tf[a] <=> @model.tf[b] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
# ++
|
|
32
|
+
# get all the words with edit_distance == 1
|
|
33
|
+
# a n-length word edit_distance == 1 nclude:
|
|
34
|
+
# a. deletion (n)
|
|
35
|
+
# b. transpositon (n-1)
|
|
36
|
+
# c. insertion (26(n+1))
|
|
37
|
+
# d. alteration (25n)
|
|
38
|
+
# ++
|
|
39
|
+
def edits1(word)
|
|
40
|
+
length = word.length
|
|
41
|
+
result = deletion(word, length) + transposition(word, length) + alteration(word, length) +
|
|
42
|
+
insertion(word, length)
|
|
43
|
+
result.empty? ? nil : result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def known_edits2(word)
|
|
47
|
+
result = []
|
|
48
|
+
edits1(word).each do |e1|
|
|
49
|
+
edits1(e1).each do
|
|
50
|
+
|e2| result << e2 if @model.tf.has_key?(e2)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
result.empty? ? nil : result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def deletion(word, word_length)
|
|
57
|
+
(0...word_length).collect do |i|
|
|
58
|
+
word[0...i] + word[ i + 1..-1]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transposition(word, word_length)
|
|
63
|
+
(0...word_length-1).collect do |i|
|
|
64
|
+
word[0...i] + word[i+1,1] + word[i,1] + word[i+2..-1]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def alteration(word, word_length)
|
|
69
|
+
alteration = []
|
|
70
|
+
word_length.times do |i|
|
|
71
|
+
@letters.each_byte do |l|
|
|
72
|
+
alteration << word[0...i]+l.chr+word[i+1..-1]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
alteration
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def insertion(word, word_length)
|
|
79
|
+
insertion = []
|
|
80
|
+
(word_length+1).times do |i|
|
|
81
|
+
@letters.each_byte do |l|
|
|
82
|
+
insertion << word[0...i]+l.chr+word[i..-1]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
insertion
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def known(words)
|
|
89
|
+
result = words.find_all {|w| @model.tf.has_key?(w) }
|
|
90
|
+
result.empty? ? nil : result
|
|
91
|
+
end
|
|
92
|
+
end
|
data/tools/icdiff
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
""" icdiff.py
|
|
4
|
+
|
|
5
|
+
Author: Jeff Kaufman, derived from difflib.HtmlDiff
|
|
6
|
+
|
|
7
|
+
License: This code is usable under the same open terms as the rest of
|
|
8
|
+
python. See: http://www.python.org/psf/license/
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import errno
|
|
15
|
+
import difflib
|
|
16
|
+
from optparse import Option, OptionParser
|
|
17
|
+
import re
|
|
18
|
+
import filecmp
|
|
19
|
+
import unicodedata
|
|
20
|
+
import codecs
|
|
21
|
+
|
|
22
|
+
__version__ = "1.7.2"
|
|
23
|
+
|
|
24
|
+
color_codes = {
|
|
25
|
+
"red": '\033[0;31m',
|
|
26
|
+
"green": '\033[0;32m',
|
|
27
|
+
"yellow": '\033[0;33m',
|
|
28
|
+
"blue": '\033[0;34m',
|
|
29
|
+
"magenta": '\033[0;35m',
|
|
30
|
+
"cyan": '\033[0;36m',
|
|
31
|
+
"none": '\033[m',
|
|
32
|
+
"red_bold": '\033[1;31m',
|
|
33
|
+
"green_bold": '\033[1;32m',
|
|
34
|
+
"yellow_bold": '\033[1;33m',
|
|
35
|
+
"blue_bold": '\033[1;34m',
|
|
36
|
+
"magenta_bold": '\033[1;35m',
|
|
37
|
+
"cyan_bold": '\033[1;36m',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class ConsoleDiff(object):
|
|
41
|
+
"""Console colored side by side comparison with change highlights.
|
|
42
|
+
|
|
43
|
+
Based on difflib.HtmlDiff
|
|
44
|
+
|
|
45
|
+
This class can be used to create a text-mode table showing a side
|
|
46
|
+
|
|
47
|
+
by side, line by line comparison of text with inter-line and
|
|
48
|
+
intra-line change highlights in ansi color escape sequences as
|
|
49
|
+
intra-line change highlights in ansi color escape sequences as
|
|
50
|
+
read by xterm. The table can be generated in either full or
|
|
51
|
+
contextual difference mode.
|
|
52
|
+
|
|
53
|
+
To generate the table, call make_table.
|
|
54
|
+
|
|
55
|
+
Usage is the almost the same as HtmlDiff except only make_table is
|
|
56
|
+
implemented and the file can be invoked on the command line.
|
|
57
|
+
Run::
|
|
58
|
+
|
|
59
|
+
python icdiff.py --help
|
|
60
|
+
|
|
61
|
+
for command line usage information.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, tabsize=8, wrapcolumn=None, linejunk=None,
|
|
66
|
+
charjunk=difflib.IS_CHARACTER_JUNK, cols=80,
|
|
67
|
+
line_numbers=False,
|
|
68
|
+
show_all_spaces=False,
|
|
69
|
+
highlight=False,
|
|
70
|
+
no_bold=False):
|
|
71
|
+
"""ConsoleDiff instance initializer
|
|
72
|
+
|
|
73
|
+
Arguments:
|
|
74
|
+
tabsize -- tab stop spacing, defaults to 8.
|
|
75
|
+
wrapcolumn -- column number where lines are broken and wrapped,
|
|
76
|
+
defaults to None where lines are not wrapped.
|
|
77
|
+
linejunk, charjunk -- keyword arguments passed into ndiff() (used by
|
|
78
|
+
ConsoleDiff() to generate the side by side differences). See
|
|
79
|
+
ndiff() documentation for argument default values and descriptions.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
self._tabsize = tabsize
|
|
83
|
+
self.line_numbers = line_numbers
|
|
84
|
+
self.cols = cols
|
|
85
|
+
self.show_all_spaces = show_all_spaces
|
|
86
|
+
self.highlight = highlight
|
|
87
|
+
self.no_bold = no_bold
|
|
88
|
+
|
|
89
|
+
if wrapcolumn is None:
|
|
90
|
+
if not line_numbers:
|
|
91
|
+
wrapcolumn = self.cols // 2 - 2
|
|
92
|
+
else:
|
|
93
|
+
wrapcolumn = self.cols // 2 - 10
|
|
94
|
+
|
|
95
|
+
self._wrapcolumn = wrapcolumn
|
|
96
|
+
self._linejunk = linejunk
|
|
97
|
+
self._charjunk = charjunk
|
|
98
|
+
|
|
99
|
+
def _tab_newline_replace(self, fromlines, tolines):
|
|
100
|
+
"""Returns from/to line lists with tabs expanded and newlines removed.
|
|
101
|
+
|
|
102
|
+
Instead of tab characters being replaced by the number of spaces
|
|
103
|
+
needed to fill in to the next tab stop, this function will fill
|
|
104
|
+
the space with tab characters. This is done so that the difference
|
|
105
|
+
algorithms can identify changes in a file when tabs are replaced by
|
|
106
|
+
spaces and vice versa. At the end of the table generation, the tab
|
|
107
|
+
characters will be replaced with a space.
|
|
108
|
+
"""
|
|
109
|
+
def expand_tabs(line):
|
|
110
|
+
# hide real spaces
|
|
111
|
+
line = line.replace(' ', '\0')
|
|
112
|
+
# expand tabs into spaces
|
|
113
|
+
line = line.expandtabs(self._tabsize)
|
|
114
|
+
# relace spaces from expanded tabs back into tab characters
|
|
115
|
+
# (we'll replace them with markup after we do differencing)
|
|
116
|
+
line = line.replace(' ', '\t')
|
|
117
|
+
return line.replace('\0', ' ').rstrip('\n')
|
|
118
|
+
fromlines = [expand_tabs(line) for line in fromlines]
|
|
119
|
+
tolines = [expand_tabs(line) for line in tolines]
|
|
120
|
+
return fromlines, tolines
|
|
121
|
+
|
|
122
|
+
def _display_len(self, s):
|
|
123
|
+
# Handle wide characters like chinese.
|
|
124
|
+
def width(c):
|
|
125
|
+
if ((isinstance(c, type(u"")) and
|
|
126
|
+
unicodedata.east_asian_width(c) == 'W')):
|
|
127
|
+
return 2
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
return sum(width(c) for c in s)
|
|
131
|
+
|
|
132
|
+
def _split_line(self, data_list, line_num, text):
|
|
133
|
+
"""Builds list of text lines by splitting text lines at wrap point
|
|
134
|
+
|
|
135
|
+
This function will determine if the input text line needs to be
|
|
136
|
+
wrapped (split) into separate lines. If so, the first wrap point
|
|
137
|
+
will be determined and the first line appended to the output
|
|
138
|
+
text line list. This function is used recursively to handle
|
|
139
|
+
the second part of the split line to further split it.
|
|
140
|
+
"""
|
|
141
|
+
# if blank line or context separator, just add it to the output list
|
|
142
|
+
if not line_num:
|
|
143
|
+
data_list.append((line_num, text))
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# if line text doesn't need wrapping, just add it to the output list
|
|
147
|
+
if ((self._display_len(text) - (text.count('\0') * 3) <=
|
|
148
|
+
self._wrapcolumn)):
|
|
149
|
+
data_list.append((line_num, text))
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# scan text looking for the wrap point, keeping track if the wrap
|
|
153
|
+
# point is inside markers
|
|
154
|
+
i = 0
|
|
155
|
+
n = 0
|
|
156
|
+
mark = ''
|
|
157
|
+
while n < self._wrapcolumn and i < len(text):
|
|
158
|
+
if text[i] == '\0':
|
|
159
|
+
i += 1
|
|
160
|
+
mark = text[i]
|
|
161
|
+
i += 1
|
|
162
|
+
elif text[i] == '\1':
|
|
163
|
+
i += 1
|
|
164
|
+
mark = ''
|
|
165
|
+
else:
|
|
166
|
+
n += self._display_len(text[i])
|
|
167
|
+
i += 1
|
|
168
|
+
|
|
169
|
+
# wrap point is inside text, break it up into separate lines
|
|
170
|
+
line1 = text[:i]
|
|
171
|
+
line2 = text[i:]
|
|
172
|
+
|
|
173
|
+
# if wrap point is inside markers, place end marker at end of first
|
|
174
|
+
# line and start marker at beginning of second line because each
|
|
175
|
+
# line will have its own table tag markup around it.
|
|
176
|
+
if mark:
|
|
177
|
+
line1 = line1 + '\1'
|
|
178
|
+
line2 = '\0' + mark + line2
|
|
179
|
+
|
|
180
|
+
# tack on first line onto the output list
|
|
181
|
+
data_list.append((line_num, line1))
|
|
182
|
+
|
|
183
|
+
# use this routine again to wrap the remaining text
|
|
184
|
+
self._split_line(data_list, '>', line2)
|
|
185
|
+
|
|
186
|
+
def _line_wrapper(self, diffs):
|
|
187
|
+
"""Returns iterator that splits (wraps) mdiff text lines"""
|
|
188
|
+
|
|
189
|
+
# pull from/to data and flags from mdiff iterator
|
|
190
|
+
for fromdata, todata, flag in diffs:
|
|
191
|
+
# check for context separators and pass them through
|
|
192
|
+
if flag is None:
|
|
193
|
+
yield fromdata, todata, flag
|
|
194
|
+
continue
|
|
195
|
+
(fromline, fromtext), (toline, totext) = fromdata, todata
|
|
196
|
+
# for each from/to line split it at the wrap column to form
|
|
197
|
+
# list of text lines.
|
|
198
|
+
fromlist, tolist = [], []
|
|
199
|
+
self._split_line(fromlist, fromline, fromtext)
|
|
200
|
+
self._split_line(tolist, toline, totext)
|
|
201
|
+
# yield from/to line in pairs inserting blank lines as
|
|
202
|
+
# necessary when one side has more wrapped lines
|
|
203
|
+
while fromlist or tolist:
|
|
204
|
+
if fromlist:
|
|
205
|
+
fromdata = fromlist.pop(0)
|
|
206
|
+
else:
|
|
207
|
+
fromdata = ('', ' ')
|
|
208
|
+
if tolist:
|
|
209
|
+
todata = tolist.pop(0)
|
|
210
|
+
else:
|
|
211
|
+
todata = ('', ' ')
|
|
212
|
+
yield fromdata, todata, flag
|
|
213
|
+
|
|
214
|
+
def _collect_lines(self, diffs):
|
|
215
|
+
"""Collects mdiff output into separate lists
|
|
216
|
+
|
|
217
|
+
Before storing the mdiff from/to data into a list, it is converted
|
|
218
|
+
into a single line of text with console markup.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
fromlist, tolist, flaglist = [], [], []
|
|
222
|
+
# pull from/to data and flags from mdiff style iterator
|
|
223
|
+
for fromdata, todata, flag in diffs:
|
|
224
|
+
if (fromdata, todata, flag) == (None, None, None):
|
|
225
|
+
yield None
|
|
226
|
+
else:
|
|
227
|
+
yield (self._format_line(*fromdata),
|
|
228
|
+
self._format_line(*todata))
|
|
229
|
+
|
|
230
|
+
def _format_line(self, linenum, text):
|
|
231
|
+
text = text.rstrip()
|
|
232
|
+
if not self.line_numbers:
|
|
233
|
+
return text
|
|
234
|
+
return self._add_line_numbers(linenum, text)
|
|
235
|
+
|
|
236
|
+
def _add_line_numbers(self, linenum, text):
|
|
237
|
+
try:
|
|
238
|
+
lid = '%d' % linenum
|
|
239
|
+
except TypeError:
|
|
240
|
+
# handle blank lines where linenum is '>' or ''
|
|
241
|
+
lid = ''
|
|
242
|
+
return text
|
|
243
|
+
return '%s %s' % (self._rpad(lid, 8), text)
|
|
244
|
+
|
|
245
|
+
def _real_len(self, s):
|
|
246
|
+
l = 0
|
|
247
|
+
in_esc = False
|
|
248
|
+
prev = ' '
|
|
249
|
+
for c in replace_all({'\0+': "",
|
|
250
|
+
'\0-': "",
|
|
251
|
+
'\0^': "",
|
|
252
|
+
'\1': "",
|
|
253
|
+
'\t': ' '}, s):
|
|
254
|
+
if in_esc:
|
|
255
|
+
if c == "m":
|
|
256
|
+
in_esc = False
|
|
257
|
+
else:
|
|
258
|
+
if c == "[" and prev == "\033":
|
|
259
|
+
in_esc = True
|
|
260
|
+
l -= 1 # we counted prev when we shouldn't have
|
|
261
|
+
else:
|
|
262
|
+
l += self._display_len(c)
|
|
263
|
+
prev = c
|
|
264
|
+
|
|
265
|
+
return l
|
|
266
|
+
|
|
267
|
+
def _rpad(self, s, field_width):
|
|
268
|
+
return self._pad(s, field_width) + s
|
|
269
|
+
|
|
270
|
+
def _pad(self, s, field_width):
|
|
271
|
+
return " " * (field_width - self._real_len(s))
|
|
272
|
+
|
|
273
|
+
def _lpad(self, s, field_width):
|
|
274
|
+
return s + self._pad(s, field_width)
|
|
275
|
+
|
|
276
|
+
def make_table(self, fromlines, tolines, fromdesc='', todesc='',
|
|
277
|
+
context=False, numlines=5):
|
|
278
|
+
"""Generates table of side by side comparison with change highlights
|
|
279
|
+
|
|
280
|
+
Arguments:
|
|
281
|
+
fromlines -- list of "from" lines
|
|
282
|
+
tolines -- list of "to" lines
|
|
283
|
+
fromdesc -- "from" file column header string
|
|
284
|
+
todesc -- "to" file column header string
|
|
285
|
+
context -- set to True for contextual differences (defaults to False
|
|
286
|
+
which shows full differences).
|
|
287
|
+
numlines -- number of context lines. When context is set True,
|
|
288
|
+
controls number of lines displayed before and after the change.
|
|
289
|
+
When context is False, controls the number of lines to place
|
|
290
|
+
the "next" link anchors before the next change (so click of
|
|
291
|
+
"next" link jumps to just before the change).
|
|
292
|
+
"""
|
|
293
|
+
if context:
|
|
294
|
+
context_lines = numlines
|
|
295
|
+
else:
|
|
296
|
+
context_lines = None
|
|
297
|
+
|
|
298
|
+
# change tabs to spaces before it gets more difficult after we insert
|
|
299
|
+
# markup
|
|
300
|
+
fromlines, tolines = self._tab_newline_replace(fromlines, tolines)
|
|
301
|
+
|
|
302
|
+
# create diffs iterator which generates side by side from/to data
|
|
303
|
+
diffs = difflib._mdiff(fromlines, tolines, context_lines,
|
|
304
|
+
linejunk=self._linejunk,
|
|
305
|
+
charjunk=self._charjunk)
|
|
306
|
+
|
|
307
|
+
# set up iterator to wrap lines that exceed desired width
|
|
308
|
+
if self._wrapcolumn:
|
|
309
|
+
diffs = self._line_wrapper(diffs)
|
|
310
|
+
diffs = self._collect_lines(diffs)
|
|
311
|
+
|
|
312
|
+
for left, right in self._generate_table(fromdesc, todesc, diffs):
|
|
313
|
+
yield self.colorize(
|
|
314
|
+
"%s %s" % (self._lpad(left, self.cols // 2 - 1),
|
|
315
|
+
self._lpad(right, self.cols // 2 - 1)))
|
|
316
|
+
|
|
317
|
+
def _generate_table(self, fromdesc, todesc, diffs):
|
|
318
|
+
if fromdesc or todesc:
|
|
319
|
+
yield (simple_colorize(fromdesc, "blue"),
|
|
320
|
+
simple_colorize(todesc, "blue"))
|
|
321
|
+
|
|
322
|
+
for i, line in enumerate(diffs):
|
|
323
|
+
if line is None:
|
|
324
|
+
# mdiff yields None on separator lines; skip the bogus ones
|
|
325
|
+
# generated for the first line
|
|
326
|
+
if i > 0:
|
|
327
|
+
yield (simple_colorize('---', "blue"),
|
|
328
|
+
simple_colorize('---', "blue"))
|
|
329
|
+
else:
|
|
330
|
+
yield line
|
|
331
|
+
|
|
332
|
+
def colorize(self, s):
|
|
333
|
+
def background(color):
|
|
334
|
+
return replace_all({"\033[1;": "\033[7;",
|
|
335
|
+
"\033[0;": "\033[7;"}, color)
|
|
336
|
+
|
|
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
|
+
|
|
346
|
+
if self.highlight:
|
|
347
|
+
C_ADD, C_SUB, C_CHG = (background(C_ADD),
|
|
348
|
+
background(C_SUB),
|
|
349
|
+
background(C_CHG))
|
|
350
|
+
|
|
351
|
+
C_NONE = color_codes["none"]
|
|
352
|
+
colors = (C_ADD, C_SUB, C_CHG, C_NONE)
|
|
353
|
+
|
|
354
|
+
s = replace_all({'\0+': C_ADD,
|
|
355
|
+
'\0-': C_SUB,
|
|
356
|
+
'\0^': C_CHG,
|
|
357
|
+
'\1': C_NONE,
|
|
358
|
+
'\t': ' '}, s)
|
|
359
|
+
|
|
360
|
+
if self.highlight:
|
|
361
|
+
return s
|
|
362
|
+
|
|
363
|
+
if not self.show_all_spaces:
|
|
364
|
+
# If there's a change consisting entirely of whitespace,
|
|
365
|
+
# don't color it.
|
|
366
|
+
return re.sub("\033\\[[01];3([123])m(\\s+)(\033\\[)",
|
|
367
|
+
"\033[7;3\\1m\\2\\3", s)
|
|
368
|
+
|
|
369
|
+
def will_see_coloredspace(i, s):
|
|
370
|
+
while i < len(s) and s[i].isspace():
|
|
371
|
+
i += 1
|
|
372
|
+
if i < len(s) and s[i] == '\033':
|
|
373
|
+
return False
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
n_s = []
|
|
377
|
+
in_color = False
|
|
378
|
+
seen_coloredspace = False
|
|
379
|
+
for i, c in enumerate(s):
|
|
380
|
+
if len(n_s) > 6 and n_s[-1] == "m":
|
|
381
|
+
ns_end = "".join(n_s[-7:])
|
|
382
|
+
for color in colors:
|
|
383
|
+
if ns_end.endswith(color):
|
|
384
|
+
if color != in_color:
|
|
385
|
+
seen_coloredspace = False
|
|
386
|
+
in_color = color
|
|
387
|
+
if ns_end.endswith(C_NONE):
|
|
388
|
+
in_color = False
|
|
389
|
+
|
|
390
|
+
if ((c.isspace() and in_color and
|
|
391
|
+
(self.show_all_spaces or not (seen_coloredspace or
|
|
392
|
+
will_see_coloredspace(i, s))))):
|
|
393
|
+
n_s.extend([C_NONE, background(in_color), c, C_NONE, in_color])
|
|
394
|
+
else:
|
|
395
|
+
if in_color:
|
|
396
|
+
seen_coloredspace = True
|
|
397
|
+
n_s.append(c)
|
|
398
|
+
|
|
399
|
+
joined = "".join(n_s)
|
|
400
|
+
|
|
401
|
+
return joined
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def simple_colorize(s, chosen_color):
|
|
405
|
+
return "%s%s%s" % (color_codes[chosen_color], s, color_codes["none"])
|
|
406
|
+
|
|
407
|
+
def replace_all(replacements, string):
|
|
408
|
+
for search, replace in replacements.items():
|
|
409
|
+
string = string.replace(search, replace)
|
|
410
|
+
return string
|
|
411
|
+
|
|
412
|
+
class MultipleOption(Option):
|
|
413
|
+
ACTIONS = Option.ACTIONS + ("extend",)
|
|
414
|
+
STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
|
|
415
|
+
TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
|
|
416
|
+
ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)
|
|
417
|
+
|
|
418
|
+
def take_action(self, action, dest, opt, value, values, parser):
|
|
419
|
+
if action == "extend":
|
|
420
|
+
values.ensure_value(dest, []).append(value)
|
|
421
|
+
else:
|
|
422
|
+
Option.take_action(self, action, dest, opt, value, values, parser)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def start():
|
|
426
|
+
# If you change any of these, also update README.
|
|
427
|
+
parser = OptionParser(usage="usage: %prog [options] left_file right_file",
|
|
428
|
+
version="icdiff version %s" % __version__,
|
|
429
|
+
description="Show differences between files in a "
|
|
430
|
+
"two column view.",
|
|
431
|
+
option_class=MultipleOption)
|
|
432
|
+
parser.add_option("--cols", default=None,
|
|
433
|
+
help="specify the width of the screen. Autodetection is "
|
|
434
|
+
"Unix only")
|
|
435
|
+
parser.add_option("--encoding", default="utf-8",
|
|
436
|
+
help="specify the file encoding; defaults to utf8")
|
|
437
|
+
parser.add_option("--head", default=0,
|
|
438
|
+
help="consider only the first N lines of each file")
|
|
439
|
+
parser.add_option("--highlight", default=False,
|
|
440
|
+
action="store_true",
|
|
441
|
+
help="color by changing the background color instead of "
|
|
442
|
+
"the foreground color. Very fast, ugly, displays all "
|
|
443
|
+
"changes")
|
|
444
|
+
parser.add_option("-L", "--label",
|
|
445
|
+
action="extend",
|
|
446
|
+
type="string",
|
|
447
|
+
dest='labels',
|
|
448
|
+
help="override file labels with arbitrary tags. "
|
|
449
|
+
"Use twice, one for each file")
|
|
450
|
+
parser.add_option("--line-numbers", default=False,
|
|
451
|
+
action="store_true",
|
|
452
|
+
help="generate output with line numbers")
|
|
453
|
+
parser.add_option("--no-bold", default=False,
|
|
454
|
+
action="store_true",
|
|
455
|
+
help="use non-bold colors; recommended for with solarized")
|
|
456
|
+
parser.add_option("--no-headers", default=False,
|
|
457
|
+
action="store_true",
|
|
458
|
+
help="don't label the left and right sides "
|
|
459
|
+
"with their file names")
|
|
460
|
+
parser.add_option("--output-encoding", default="utf-8",
|
|
461
|
+
help="specify the output encoding; defaults to utf8")
|
|
462
|
+
parser.add_option("--recursive", default=False,
|
|
463
|
+
action="store_true",
|
|
464
|
+
help="recursively compare subdirectories")
|
|
465
|
+
parser.add_option("--show-all-spaces", default=False,
|
|
466
|
+
action="store_true",
|
|
467
|
+
help="color all non-matching whitespace including "
|
|
468
|
+
"that which is not needed for drawing the eye to "
|
|
469
|
+
"changes. Slow, ugly, displays all changes")
|
|
470
|
+
parser.add_option("-u", "--patch", default=True,
|
|
471
|
+
action="store_true",
|
|
472
|
+
help="generate patch. This is always true, "
|
|
473
|
+
"and only exists for compatibility")
|
|
474
|
+
parser.add_option("-U", "--unified", "--numlines", default=5,
|
|
475
|
+
metavar="NUM",
|
|
476
|
+
help="how many lines of context to print; "
|
|
477
|
+
"can't be combined with --whole-file")
|
|
478
|
+
parser.add_option("--whole-file", default=False,
|
|
479
|
+
action="store_true",
|
|
480
|
+
help="show the whole file instead of just changed "
|
|
481
|
+
"lines and context")
|
|
482
|
+
|
|
483
|
+
(options, args) = parser.parse_args()
|
|
484
|
+
|
|
485
|
+
if len(args) != 2:
|
|
486
|
+
parser.print_help()
|
|
487
|
+
sys.exit()
|
|
488
|
+
|
|
489
|
+
a, b = args
|
|
490
|
+
|
|
491
|
+
if not options.cols:
|
|
492
|
+
def ioctl_GWINSZ(fd):
|
|
493
|
+
try:
|
|
494
|
+
import fcntl
|
|
495
|
+
import termios
|
|
496
|
+
import struct
|
|
497
|
+
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
|
|
498
|
+
'1234'))
|
|
499
|
+
except Exception:
|
|
500
|
+
return None
|
|
501
|
+
return cr
|
|
502
|
+
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
|
503
|
+
if cr:
|
|
504
|
+
options.cols = cr[1]
|
|
505
|
+
else:
|
|
506
|
+
options.cols = 80
|
|
507
|
+
|
|
508
|
+
if options.recursive:
|
|
509
|
+
diff_recursively(options, a, b)
|
|
510
|
+
else:
|
|
511
|
+
diff_files(options, a, b)
|
|
512
|
+
|
|
513
|
+
def codec_print(s, options):
|
|
514
|
+
s = "%s\n" % s
|
|
515
|
+
if hasattr(sys.stdout, "buffer"):
|
|
516
|
+
sys.stdout.buffer.write(s.encode(options.output_encoding))
|
|
517
|
+
else:
|
|
518
|
+
sys.stdout.write(s.encode(options.output_encoding))
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def diff_recursively(options, a, b):
|
|
522
|
+
def print_meta(s):
|
|
523
|
+
codec_print(simple_colorize(s, "magenta"), options)
|
|
524
|
+
|
|
525
|
+
if os.path.isfile(a) and os.path.isfile(b):
|
|
526
|
+
if not filecmp.cmp(a, b, shallow=False):
|
|
527
|
+
diff_files(options, a, b)
|
|
528
|
+
|
|
529
|
+
elif os.path.isdir(a) and os.path.isdir(b):
|
|
530
|
+
a_contents = set(os.listdir(a))
|
|
531
|
+
b_contents = set(os.listdir(b))
|
|
532
|
+
|
|
533
|
+
for child in sorted(a_contents.union(b_contents)):
|
|
534
|
+
if child not in b_contents:
|
|
535
|
+
print_meta("Only in %s: %s" % (a, child))
|
|
536
|
+
elif child not in a_contents:
|
|
537
|
+
print_meta("Only in %s: %s" % (b, child))
|
|
538
|
+
else:
|
|
539
|
+
diff_recursively(options,
|
|
540
|
+
os.path.join(a, child),
|
|
541
|
+
os.path.join(b, child))
|
|
542
|
+
elif os.path.isdir(a) and os.path.isfile(b):
|
|
543
|
+
print_meta("File %s is a directory while %s is a file" % (a, b))
|
|
544
|
+
|
|
545
|
+
elif os.path.isfile(a) and os.path.isdir(b):
|
|
546
|
+
print_meta("File %s is a file while %s is a directory" % (a, b))
|
|
547
|
+
|
|
548
|
+
def read_file(fname, options):
|
|
549
|
+
try:
|
|
550
|
+
with codecs.open(fname, encoding=options.encoding, mode="rb") as inf:
|
|
551
|
+
return inf.readlines()
|
|
552
|
+
except UnicodeDecodeError as e:
|
|
553
|
+
codec_print(
|
|
554
|
+
"error: file '%s' not valid with encoding '%s': <%s> at %s-%s." %
|
|
555
|
+
(fname, options.encoding, e.reason, e.start, e.end), options)
|
|
556
|
+
raise
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def diff_files(options, a, b):
|
|
560
|
+
if options.labels:
|
|
561
|
+
if len(options.labels) == 2:
|
|
562
|
+
headers = options.labels
|
|
563
|
+
else:
|
|
564
|
+
codec_print("error: to use arbitrary file labels, "
|
|
565
|
+
"specify -L twice.", options)
|
|
566
|
+
return
|
|
567
|
+
else:
|
|
568
|
+
headers = a, b
|
|
569
|
+
if options.no_headers:
|
|
570
|
+
headers = None, None
|
|
571
|
+
|
|
572
|
+
head = int(options.head)
|
|
573
|
+
|
|
574
|
+
for x in [a, b]:
|
|
575
|
+
if os.path.isdir(x):
|
|
576
|
+
codec_print("error: %s is a directory; did you mean to "
|
|
577
|
+
"pass --recursive?" % x, optoins)
|
|
578
|
+
return
|
|
579
|
+
try:
|
|
580
|
+
lines_a = read_file(a, options)
|
|
581
|
+
lines_b = read_file(b, options)
|
|
582
|
+
except UnicodeDecodeError:
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
if head != 0:
|
|
586
|
+
lines_a = lines_a[:head]
|
|
587
|
+
lines_b = lines_b[:head]
|
|
588
|
+
|
|
589
|
+
cd = ConsoleDiff(cols=int(options.cols),
|
|
590
|
+
show_all_spaces=options.show_all_spaces,
|
|
591
|
+
highlight=options.highlight,
|
|
592
|
+
no_bold=options.no_bold,
|
|
593
|
+
line_numbers=options.line_numbers)
|
|
594
|
+
for line in cd.make_table(
|
|
595
|
+
lines_a, lines_b, headers[0], headers[1],
|
|
596
|
+
context=(not options.whole_file),
|
|
597
|
+
numlines=int(options.unified)):
|
|
598
|
+
codec_print(line, options)
|
|
599
|
+
sys.stdout.flush()
|
|
600
|
+
|
|
601
|
+
if __name__ == "__main__":
|
|
602
|
+
try:
|
|
603
|
+
start()
|
|
604
|
+
except KeyboardInterrupt:
|
|
605
|
+
pass
|
|
606
|
+
except IOError as e:
|
|
607
|
+
if e.errno == errno.EPIPE:
|
|
608
|
+
pass
|
|
609
|
+
else:
|
|
610
|
+
raise
|