]> git.rmz.io Git - dotfiles.git/blob - weechat/python/grep.py
vim: search vim help
[dotfiles.git] / weechat / python / grep.py
1 # -*- coding: utf-8 -*-
2 ###
3 # Copyright (c) 2009-2011 by Elián Hanisch <lambdae2@gmail.com>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 ###
18
19 ###
20 # Search in Weechat buffers and logs (for Weechat 0.3.*)
21 #
22 # Inspired by xt's grep.py
23 # Originally I just wanted to add some fixes in grep.py, but then
24 # I got carried away and rewrote everything, so new script.
25 #
26 # Commands:
27 # * /grep
28 # Search in logs or buffers, see /help grep
29 # * /logs:
30 # Lists logs in ~/.weechat/logs, see /help logs
31 #
32 # Settings:
33 # * plugins.var.python.grep.clear_buffer:
34 # Clear the results buffer before each search. Valid values: on, off
35 #
36 # * plugins.var.python.grep.go_to_buffer:
37 # Automatically go to grep buffer when search is over. Valid values: on, off
38 #
39 # * plugins.var.python.grep.log_filter:
40 # Coma separated list of patterns that grep will use for exclude logs, e.g.
41 # if you use '*server/*' any log in the 'server' folder will be excluded
42 # when using the command '/grep log'
43 #
44 # * plugins.var.python.grep.show_summary:
45 # Shows summary for each log. Valid values: on, off
46 #
47 # * plugins.var.python.grep.max_lines:
48 # Grep will only print the last matched lines that don't surpass the value defined here.
49 #
50 # * plugins.var.python.grep.size_limit:
51 # Size limit in KiB, is used for decide whenever grepping should run in background or not. If
52 # the logs to grep have a total size bigger than this value then grep run as a new process.
53 # It can be used for force or disable background process, using '0' forces to always grep in
54 # background, while using '' (empty string) will disable it.
55 #
56 # * plugins.var.python.grep.default_tail_head:
57 # Config option for define default number of lines returned when using --head or --tail options.
58 # Can be overriden in the command with --number option.
59 #
60 #
61 # TODO:
62 # * try to figure out why hook_process chokes in long outputs (using a tempfile as a
63 # workaround now)
64 # * possibly add option for defining time intervals
65 #
66 #
67 # History:
68 # 2014-03-29, Felix Eckhofer <felix@tribut.de>
69 # version 0.7.3: fix typo
70 #
71 # 2011-01-09
72 # version 0.7.2: bug fixes
73 #
74 # 2010-11-15
75 # version 0.7.1:
76 # * use TempFile so temporal files are guaranteed to be deleted.
77 # * enable Archlinux workaround.
78 #
79 # 2010-10-26
80 # version 0.7:
81 # * added templates.
82 # * using --only-match shows only unique strings.
83 # * fixed bug that inverted -B -A switches when used with -t
84 #
85 # 2010-10-14
86 # version 0.6.8: by xt <xt@bash.no>
87 # * supress highlights when printing in grep buffer
88 #
89 # 2010-10-06
90 # version 0.6.7: by xt <xt@bash.no>
91 # * better temporary file:
92 # use tempfile.mkstemp. to create a temp file in log dir,
93 # makes it safer with regards to write permission and multi user
94 #
95 # 2010-04-08
96 # version 0.6.6: bug fixes
97 # * use WEECHAT_LIST_POS_END in log file completion, makes completion faster
98 # * disable bytecode if using python 2.6
99 # * use single quotes in command string
100 # * fix bug that could change buffer's title when using /grep stop
101 #
102 # 2010-01-24
103 # version 0.6.5: disable bytecode is a 2.6 feature, instead, resort to delete the bytecode manually
104 #
105 # 2010-01-19
106 # version 0.6.4: bug fix
107 # version 0.6.3: added options --invert --only-match (replaces --exact, which is still available
108 # but removed from help)
109 # * use new 'irc_nick_color' info
110 # * don't generate bytecode when spawning a new process
111 # * show active options in buffer title
112 #
113 # 2010-01-17
114 # version 0.6.2: removed 2.6-ish code
115 # version 0.6.1: fixed bug when grepping in grep's buffer
116 #
117 # 2010-01-14
118 # version 0.6.0: implemented grep in background
119 # * improved context lines presentation.
120 # * grepping for big (or many) log files runs in a weechat_process.
121 # * added /grep stop.
122 # * added 'size_limit' option
123 # * fixed a infolist leak when grepping buffers
124 # * added 'default_tail_head' option
125 # * results are sort by line count
126 # * don't die if log is corrupted (has NULL chars in it)
127 # * changed presentation of /logs
128 # * log path completion doesn't suck anymore
129 # * removed all tabs, because I learned how to configure Vim so that spaces aren't annoying
130 # anymore. This was the script's original policy.
131 #
132 # 2010-01-05
133 # version 0.5.5: rename script to 'grep.py' (FlashCode <flashcode@flashtux.org>).
134 #
135 # 2010-01-04
136 # version 0.5.4.1: fix index error when using --after/before-context options.
137 #
138 # 2010-01-03
139 # version 0.5.4: new features
140 # * added --after-context and --before-context options.
141 # * added --context as a shortcut for using both -A -B options.
142 #
143 # 2009-11-06
144 # version 0.5.3: improvements for long grep output
145 # * grep buffer input accepts the same flags as /grep for repeat a search with different
146 # options.
147 # * tweaks in grep's output.
148 # * max_lines option added for limit grep's output.
149 # * code in update_buffer() optimized.
150 # * time stats in buffer title.
151 # * added go_to_buffer config option.
152 # * added --buffer for search only in buffers.
153 # * refactoring.
154 #
155 # 2009-10-12, omero
156 # version 0.5.2: made it python-2.4.x compliant
157 #
158 # 2009-08-17
159 # version 0.5.1: some refactoring, show_summary option added.
160 #
161 # 2009-08-13
162 # version 0.5: rewritten from xt's grep.py
163 # * fixed searching in non weechat logs, for cases like, if you're
164 # switching from irssi and rename and copy your irssi logs to %h/logs
165 # * fixed "timestamp rainbow" when you /grep in grep's buffer
166 # * allow to search in other buffers other than current or in logs
167 # of currently closed buffers with cmd 'buffer'
168 # * allow to search in any log file in %h/logs with cmd 'log'
169 # * added --count for return the number of matched lines
170 # * added --matchcase for case sensible search
171 # * added --hilight for color matches
172 # * added --head and --tail options, and --number
173 # * added command /logs for list files in %h/logs
174 # * added config option for clear the buffer before a search
175 # * added config option for filter logs we don't want to grep
176 # * added the posibility to repeat last search with another regexp by writing
177 # it in grep's buffer
178 # * changed spaces for tabs in the code, which is my preference
179 #
180 ###
181
182 from os import path
183 import sys, getopt, time, os, re, tempfile
184
185 try:
186 import weechat
187 from weechat import WEECHAT_RC_OK, prnt, prnt_date_tags
188 import_ok = True
189 except ImportError:
190 import_ok = False
191
192 SCRIPT_NAME = "grep"
193 SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
194 SCRIPT_VERSION = "0.7.3"
195 SCRIPT_LICENSE = "GPL3"
196 SCRIPT_DESC = "Search in buffers and logs"
197 SCRIPT_COMMAND = "grep"
198
199 ### Default Settings ###
200 settings = {
201 'clear_buffer' : 'off',
202 'log_filter' : '',
203 'go_to_buffer' : 'on',
204 'max_lines' : '4000',
205 'show_summary' : 'on',
206 'size_limit' : '2048',
207 'default_tail_head' : '10',
208 }
209
210 ### Class definitions ###
211 class linesDict(dict):
212 """
213 Class for handling matched lines in more than one buffer.
214 linesDict[buffer_name] = matched_lines_list
215 """
216 def __setitem__(self, key, value):
217 assert isinstance(value, list)
218 if key not in self:
219 dict.__setitem__(self, key, value)
220 else:
221 dict.__getitem__(self, key).extend(value)
222
223 def get_matches_count(self):
224 """Return the sum of total matches stored."""
225 if dict.__len__(self):
226 return sum(map(lambda L: L.matches_count, self.itervalues()))
227 else:
228 return 0
229
230 def __len__(self):
231 """Return the sum of total lines stored."""
232 if dict.__len__(self):
233 return sum(map(len, self.itervalues()))
234 else:
235 return 0
236
237 def __str__(self):
238 """Returns buffer count or buffer name if there's just one stored."""
239 n = len(self.keys())
240 if n == 1:
241 return self.keys()[0]
242 elif n > 1:
243 return '%s logs' %n
244 else:
245 return ''
246
247 def items(self):
248 """Returns a list of items sorted by line count."""
249 items = dict.items(self)
250 items.sort(key=lambda i: len(i[1]))
251 return items
252
253 def items_count(self):
254 """Returns a list of items sorted by match count."""
255 items = dict.items(self)
256 items.sort(key=lambda i: i[1].matches_count)
257 return items
258
259 def strip_separator(self):
260 for L in self.itervalues():
261 L.strip_separator()
262
263 def get_last_lines(self, n):
264 total_lines = len(self)
265 #debug('total: %s n: %s' %(total_lines, n))
266 if n >= total_lines:
267 # nothing to do
268 return
269 for k, v in reversed(self.items()):
270 l = len(v)
271 if n > 0:
272 if l > n:
273 del v[:l-n]
274 v.stripped_lines = l-n
275 n -= l
276 else:
277 del v[:]
278 v.stripped_lines = l
279
280 class linesList(list):
281 """Class for list of matches, since sometimes I need to add lines that aren't matches, I need an
282 independent counter."""
283 _sep = '...'
284 def __init__(self, *args):
285 list.__init__(self, *args)
286 self.matches_count = 0
287 self.stripped_lines = 0
288
289 def append(self, item):
290 """Append lines, can be a string or a list with strings."""
291 if isinstance(item, str):
292 list.append(self, item)
293 else:
294 self.extend(item)
295
296 def append_separator(self):
297 """adds a separator into the list, makes sure it doen't add two together."""
298 s = self._sep
299 if (self and self[-1] != s) or not self:
300 self.append(s)
301
302 def onlyUniq(self):
303 s = set(self)
304 del self[:]
305 self.extend(s)
306
307 def count_match(self, item=None):
308 if item is None or isinstance(item, str):
309 self.matches_count += 1
310 else:
311 self.matches_count += len(item)
312
313 def strip_separator(self):
314 """removes separators if there are first or/and last in the list."""
315 if self:
316 s = self._sep
317 if self[0] == s:
318 del self[0]
319 if self[-1] == s:
320 del self[-1]
321
322 ### Misc functions ###
323 now = time.time
324 def get_size(f):
325 try:
326 return os.stat(f).st_size
327 except OSError:
328 return 0
329
330 sizeDict = {0:'b', 1:'KiB', 2:'MiB', 3:'GiB', 4:'TiB'}
331 def human_readable_size(size):
332 power = 0
333 while size > 1024:
334 power += 1
335 size /= 1024.0
336 return '%.2f %s' %(size, sizeDict.get(power, ''))
337
338 def color_nick(nick):
339 """Returns coloured nick, with coloured mode if any."""
340 if not nick: return ''
341 wcolor = weechat.color
342 config_string = lambda s : weechat.config_string(weechat.config_get(s))
343 config_int = lambda s : weechat.config_integer(weechat.config_get(s))
344 # prefix and suffix
345 prefix = config_string('irc.look.nick_prefix')
346 suffix = config_string('irc.look.nick_suffix')
347 prefix_c = suffix_c = wcolor(config_string('weechat.color.chat_delimiters'))
348 if nick[0] == prefix:
349 nick = nick[1:]
350 else:
351 prefix = prefix_c = ''
352 if nick[-1] == suffix:
353 nick = nick[:-1]
354 suffix = wcolor(color_delimiter) + suffix
355 else:
356 suffix = suffix_c = ''
357 # nick mode
358 modes = '@!+%'
359 if nick[0] in modes:
360 mode, nick = nick[0], nick[1:]
361 mode_color = wcolor(config_string('weechat.color.nicklist_prefix%d' \
362 %(modes.find(mode) + 1)))
363 else:
364 mode = mode_color = ''
365 # nick color
366 nick_color = weechat.info_get('irc_nick_color', nick)
367 if not nick_color:
368 # probably we're in WeeChat 0.3.0
369 #debug('no irc_nick_color')
370 color_nicks_number = config_int('weechat.look.color_nicks_number')
371 idx = (sum(map(ord, nick))%color_nicks_number) + 1
372 nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx))
373 return ''.join((prefix_c, prefix, mode_color, mode, nick_color, nick, suffix_c, suffix))
374
375 ### Config and value validation ###
376 boolDict = {'on':True, 'off':False}
377 def get_config_boolean(config):
378 value = weechat.config_get_plugin(config)
379 try:
380 return boolDict[value]
381 except KeyError:
382 default = settings[config]
383 error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
384 error("'%s' is invalid, allowed: 'on', 'off'" %value)
385 return boolDict[default]
386
387 def get_config_int(config, allow_empty_string=False):
388 value = weechat.config_get_plugin(config)
389 try:
390 return int(value)
391 except ValueError:
392 if value == '' and allow_empty_string:
393 return value
394 default = settings[config]
395 error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
396 error("'%s' is not a number." %value)
397 return int(default)
398
399 def get_config_log_filter():
400 filter = weechat.config_get_plugin('log_filter')
401 if filter:
402 return filter.split(',')
403 else:
404 return []
405
406 def get_home():
407 home = weechat.config_string(weechat.config_get('logger.file.path'))
408 return home.replace('%h', weechat.info_get('weechat_dir', ''))
409
410 def strip_home(s, dir=''):
411 """Strips home dir from the begging of the log path, this makes them sorter."""
412 if not dir:
413 global home_dir
414 dir = home_dir
415 l = len(dir)
416 if s[:l] == dir:
417 return s[l:]
418 return s
419
420 ### Messages ###
421 script_nick = SCRIPT_NAME
422 def error(s, buffer=''):
423 """Error msg"""
424 prnt(buffer, '%s%s %s' %(weechat.prefix('error'), script_nick, s))
425 if weechat.config_get_plugin('debug'):
426 import traceback
427 if traceback.sys.exc_type:
428 trace = traceback.format_exc()
429 prnt('', trace)
430
431 def say(s, buffer=''):
432 """normal msg"""
433 prnt_date_tags(buffer, 0, 'no_highlight', '%s\t%s' %(script_nick, s))
434
435
436
437 ### Log files and buffers ###
438 cache_dir = {} # note: don't remove, needed for completion if the script was loaded recently
439 def dir_list(dir, filter_list=(), filter_excludes=True, include_dir=False):
440 """Returns a list of files in 'dir' and its subdirs."""
441 global cache_dir
442 from os import walk
443 from fnmatch import fnmatch
444 #debug('dir_list: listing in %s' %dir)
445 key = (dir, include_dir)
446 try:
447 return cache_dir[key]
448 except KeyError:
449 pass
450
451 filter_list = filter_list or get_config_log_filter()
452 dir_len = len(dir)
453 if filter_list:
454 def filter(file):
455 file = file[dir_len:] # pattern shouldn't match home dir
456 for pattern in filter_list:
457 if fnmatch(file, pattern):
458 return filter_excludes
459 return not filter_excludes
460 else:
461 filter = lambda f : not filter_excludes
462
463 file_list = []
464 extend = file_list.extend
465 join = path.join
466 def walk_path():
467 for basedir, subdirs, files in walk(dir):
468 #if include_dir:
469 # subdirs = map(lambda s : join(s, ''), subdirs)
470 # files.extend(subdirs)
471 files_path = map(lambda f : join(basedir, f), files)
472 files_path = [ file for file in files_path if not filter(file) ]
473 extend(files_path)
474
475 walk_path()
476 cache_dir[key] = file_list
477 #debug('dir_list: got %s' %str(file_list))
478 return file_list
479
480 def get_file_by_pattern(pattern, all=False):
481 """Returns the first log whose path matches 'pattern',
482 if all is True returns all logs that matches."""
483 if not pattern: return []
484 #debug('get_file_by_filename: searching for %s.' %pattern)
485 # do envvar expandsion and check file
486 file = path.expanduser(pattern)
487 file = path.expandvars(file)
488 if path.isfile(file):
489 return [file]
490 # lets see if there's a matching log
491 global home_dir
492 file = path.join(home_dir, pattern)
493 if path.isfile(file):
494 return [file]
495 else:
496 from fnmatch import fnmatch
497 file = []
498 file_list = dir_list(home_dir)
499 n = len(home_dir)
500 for log in file_list:
501 basename = log[n:]
502 if fnmatch(basename, pattern):
503 file.append(log)
504 #debug('get_file_by_filename: got %s.' %file)
505 if not all and file:
506 file.sort()
507 return [ file[-1] ]
508 return file
509
510 def get_file_by_buffer(buffer):
511 """Given buffer pointer, finds log's path or returns None."""
512 #debug('get_file_by_buffer: searching for %s' %buffer)
513 infolist = weechat.infolist_get('logger_buffer', '', '')
514 if not infolist: return
515 try:
516 while weechat.infolist_next(infolist):
517 pointer = weechat.infolist_pointer(infolist, 'buffer')
518 if pointer == buffer:
519 file = weechat.infolist_string(infolist, 'log_filename')
520 if weechat.infolist_integer(infolist, 'log_enabled'):
521 #debug('get_file_by_buffer: got %s' %file)
522 return file
523 #else:
524 # debug('get_file_by_buffer: got %s but log not enabled' %file)
525 finally:
526 #debug('infolist gets freed')
527 weechat.infolist_free(infolist)
528
529 def get_file_by_name(buffer_name):
530 """Given a buffer name, returns its log path or None. buffer_name should be in 'server.#channel'
531 or '#channel' format."""
532 #debug('get_file_by_name: searching for %s' %buffer_name)
533 # common mask options
534 config_masks = ('logger.mask.irc', 'logger.file.mask')
535 # since there's no buffer pointer, we try to replace some local vars in mask, like $channel and
536 # $server, then replace the local vars left with '*', and use it as a mask for get the path with
537 # get_file_by_pattern
538 for config in config_masks:
539 mask = weechat.config_string(weechat.config_get(config))
540 #debug('get_file_by_name: mask: %s' %mask)
541 if '$name' in mask:
542 mask = mask.replace('$name', buffer_name)
543 elif '$channel' in mask or '$server' in mask:
544 if '.' in buffer_name and \
545 '#' not in buffer_name[:buffer_name.find('.')]: # the dot isn't part of the channel name
546 # ^ I'm asuming channel starts with #, i'm lazy.
547 server, channel = buffer_name.split('.', 1)
548 else:
549 server, channel = '*', buffer_name
550 if '$channel' in mask:
551 mask = mask.replace('$channel', channel)
552 if '$server' in mask:
553 mask = mask.replace('$server', server)
554 # change the unreplaced vars by '*'
555 from string import letters
556 if '%' in mask:
557 # vars for time formatting
558 mask = mask.replace('%', '$')
559 if '$' in mask:
560 masks = mask.split('$')
561 masks = map(lambda s: s.lstrip(letters), masks)
562 mask = '*'.join(masks)
563 if mask[0] != '*':
564 mask = '*' + mask
565 #debug('get_file_by_name: using mask %s' %mask)
566 file = get_file_by_pattern(mask)
567 #debug('get_file_by_name: got file %s' %file)
568 if file:
569 return file
570 return None
571
572 def get_buffer_by_name(buffer_name):
573 """Given a buffer name returns its buffer pointer or None."""
574 #debug('get_buffer_by_name: searching for %s' %buffer_name)
575 pointer = weechat.buffer_search('', buffer_name)
576 if not pointer:
577 try:
578 infolist = weechat.infolist_get('buffer', '', '')
579 while weechat.infolist_next(infolist):
580 short_name = weechat.infolist_string(infolist, 'short_name')
581 name = weechat.infolist_string(infolist, 'name')
582 if buffer_name in (short_name, name):
583 #debug('get_buffer_by_name: found %s' %name)
584 pointer = weechat.buffer_search('', name)
585 return pointer
586 finally:
587 weechat.infolist_free(infolist)
588 #debug('get_buffer_by_name: got %s' %pointer)
589 return pointer
590
591 def get_all_buffers():
592 """Returns list with pointers of all open buffers."""
593 buffers = []
594 infolist = weechat.infolist_get('buffer', '', '')
595 while weechat.infolist_next(infolist):
596 buffers.append(weechat.infolist_pointer(infolist, 'pointer'))
597 weechat.infolist_free(infolist)
598 grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
599 if grep_buffer and grep_buffer in buffers:
600 # remove it from list
601 del buffers[buffers.index(grep_buffer)]
602 return buffers
603
604 ### Grep ###
605 def make_regexp(pattern, matchcase=False):
606 """Returns a compiled regexp."""
607 if pattern in ('.', '.*', '.?', '.+'):
608 # because I don't need to use a regexp if we're going to match all lines
609 return None
610 # matching takes a lot more time if pattern starts or ends with .* and it isn't needed.
611 if pattern[:2] == '.*':
612 pattern = pattern[2:]
613 if pattern[-2:] == '.*':
614 pattern = pattern[:-2]
615 try:
616 if not matchcase:
617 regexp = re.compile(pattern, re.IGNORECASE)
618 else:
619 regexp = re.compile(pattern)
620 except Exception, e:
621 raise Exception, 'Bad pattern, %s' %e
622 return regexp
623
624 def check_string(s, regexp, hilight='', exact=False):
625 """Checks 's' with a regexp and returns it if is a match."""
626 if not regexp:
627 return s
628
629 elif exact:
630 matchlist = regexp.findall(s)
631 if matchlist:
632 if isinstance(matchlist[0], tuple):
633 # join tuples (when there's more than one match group in regexp)
634 return [ ' '.join(t) for t in matchlist ]
635 return matchlist
636
637 elif hilight:
638 matchlist = regexp.findall(s)
639 if matchlist:
640 if isinstance(matchlist[0], tuple):
641 # flatten matchlist
642 matchlist = [ item for L in matchlist for item in L if item ]
643 matchlist = list(set(matchlist)) # remove duplicates if any
644 # apply hilight
645 color_hilight, color_reset = hilight.split(',', 1)
646 for m in matchlist:
647 s = s.replace(m, '%s%s%s' % (color_hilight, m, color_reset))
648 return s
649
650 # no need for findall() here
651 elif regexp.search(s):
652 return s
653
654 def grep_file(file, head, tail, after_context, before_context, count, regexp, hilight, exact, invert):
655 """Return a list of lines that match 'regexp' in 'file', if no regexp returns all lines."""
656 if count:
657 tail = head = after_context = before_context = False
658 hilight = ''
659 elif exact:
660 before_context = after_context = False
661 hilight = ''
662 elif invert:
663 hilight = ''
664 #debug(' '.join(map(str, (file, head, tail, after_context, before_context))))
665
666 lines = linesList()
667 # define these locally as it makes the loop run slightly faster
668 append = lines.append
669 count_match = lines.count_match
670 separator = lines.append_separator
671 if invert:
672 def check(s):
673 if check_string(s, regexp, hilight, exact):
674 return None
675 else:
676 return s
677 else:
678 check = lambda s: check_string(s, regexp, hilight, exact)
679
680 try:
681 file_object = open(file, 'r')
682 except IOError:
683 # file doesn't exist
684 return lines
685 if tail or before_context:
686 # for these options, I need to seek in the file, but is slower and uses a good deal of
687 # memory if the log is too big, so we do this *only* for these options.
688 file_lines = file_object.readlines()
689
690 if tail:
691 # instead of searching in the whole file and later pick the last few lines, we
692 # reverse the log, search until count reached and reverse it again, that way is a lot
693 # faster
694 file_lines.reverse()
695 # don't invert context switches
696 before_context, after_context = after_context, before_context
697
698 if before_context:
699 before_context_range = range(1, before_context + 1)
700 before_context_range.reverse()
701
702 limit = tail or head
703
704 line_idx = 0
705 while line_idx < len(file_lines):
706 line = file_lines[line_idx]
707 line = check(line)
708 if line:
709 if before_context:
710 separator()
711 trimmed = False
712 for id in before_context_range:
713 try:
714 context_line = file_lines[line_idx - id]
715 if check(context_line):
716 # match in before context, that means we appended these same lines in a
717 # previous match, so we delete them merging both paragraphs
718 if not trimmed:
719 del lines[id - before_context - 1:]
720 trimmed = True
721 else:
722 append(context_line)
723 except IndexError:
724 pass
725 append(line)
726 count_match(line)
727 if after_context:
728 id, offset = 0, 0
729 while id < after_context + offset:
730 id += 1
731 try:
732 context_line = file_lines[line_idx + id]
733 _context_line = check(context_line)
734 if _context_line:
735 offset = id
736 context_line = _context_line # so match is hilighted with --hilight
737 count_match()
738 append(context_line)
739 except IndexError:
740 pass
741 separator()
742 line_idx += id
743 if limit and lines.matches_count >= limit:
744 break
745 line_idx += 1
746
747 if tail:
748 lines.reverse()
749 else:
750 # do a normal grep
751 limit = head
752
753 for line in file_object:
754 line = check(line)
755 if line:
756 count or append(line)
757 count_match(line)
758 if after_context:
759 id, offset = 0, 0
760 while id < after_context + offset:
761 id += 1
762 try:
763 context_line = file_object.next()
764 _context_line = check(context_line)
765 if _context_line:
766 offset = id
767 context_line = _context_line
768 count_match()
769 count or append(context_line)
770 except StopIteration:
771 pass
772 separator()
773 if limit and lines.matches_count >= limit:
774 break
775
776 file_object.close()
777 return lines
778
779 def grep_buffer(buffer, head, tail, after_context, before_context, count, regexp, hilight, exact,
780 invert):
781 """Return a list of lines that match 'regexp' in 'buffer', if no regexp returns all lines."""
782 lines = linesList()
783 if count:
784 tail = head = after_context = before_context = False
785 hilight = ''
786 elif exact:
787 before_context = after_context = False
788 #debug(' '.join(map(str, (tail, head, after_context, before_context, count, exact, hilight))))
789
790 # Using /grep in grep's buffer can lead to some funny effects
791 # We should take measures if that's the case
792 def make_get_line_funcion():
793 """Returns a function for get lines from the infolist, depending if the buffer is grep's or
794 not."""
795 string_remove_color = weechat.string_remove_color
796 infolist_string = weechat.infolist_string
797 grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
798 if grep_buffer and buffer == grep_buffer:
799 def function(infolist):
800 prefix = infolist_string(infolist, 'prefix')
801 message = infolist_string(infolist, 'message')
802 if prefix: # only our messages have prefix, ignore it
803 return None
804 return message
805 else:
806 infolist_time = weechat.infolist_time
807 def function(infolist):
808 prefix = string_remove_color(infolist_string(infolist, 'prefix'), '')
809 message = string_remove_color(infolist_string(infolist, 'message'), '')
810 date = infolist_time(infolist, 'date')
811 return '%s\t%s\t%s' %(date, prefix, message)
812 return function
813 get_line = make_get_line_funcion()
814
815 infolist = weechat.infolist_get('buffer_lines', buffer, '')
816 if tail:
817 # like with grep_file() if we need the last few matching lines, we move the cursor to
818 # the end and search backwards
819 infolist_next = weechat.infolist_prev
820 infolist_prev = weechat.infolist_next
821 else:
822 infolist_next = weechat.infolist_next
823 infolist_prev = weechat.infolist_prev
824 limit = head or tail
825
826 # define these locally as it makes the loop run slightly faster
827 append = lines.append
828 count_match = lines.count_match
829 separator = lines.append_separator
830 if invert:
831 def check(s):
832 if check_string(s, regexp, hilight, exact):
833 return None
834 else:
835 return s
836 else:
837 check = lambda s: check_string(s, regexp, hilight, exact)
838
839 if before_context:
840 before_context_range = range(1, before_context + 1)
841 before_context_range.reverse()
842
843 while infolist_next(infolist):
844 line = get_line(infolist)
845 if line is None: continue
846 line = check(line)
847 if line:
848 if before_context:
849 separator()
850 trimmed = False
851 for id in before_context_range:
852 if not infolist_prev(infolist):
853 trimmed = True
854 for id in before_context_range:
855 context_line = get_line(infolist)
856 if check(context_line):
857 if not trimmed:
858 del lines[id - before_context - 1:]
859 trimmed = True
860 else:
861 append(context_line)
862 infolist_next(infolist)
863 count or append(line)
864 count_match(line)
865 if after_context:
866 id, offset = 0, 0
867 while id < after_context + offset:
868 id += 1
869 if infolist_next(infolist):
870 context_line = get_line(infolist)
871 _context_line = check(context_line)
872 if _context_line:
873 context_line = _context_line
874 offset = id
875 count_match()
876 append(context_line)
877 else:
878 # in the main loop infolist_next will start again an cause an infinite loop
879 # this will avoid it
880 infolist_next = lambda x: 0
881 separator()
882 if limit and lines.matches_count >= limit:
883 break
884 weechat.infolist_free(infolist)
885
886 if tail:
887 lines.reverse()
888 return lines
889
890 ### this is our main grep function
891 hook_file_grep = None
892 def show_matching_lines():
893 """
894 Greps buffers in search_in_buffers or files in search_in_files and updates grep buffer with the
895 result.
896 """
897 global pattern, matchcase, number, count, exact, hilight, invert
898 global tail, head, after_context, before_context
899 global search_in_files, search_in_buffers, matched_lines, home_dir
900 global time_start
901 matched_lines = linesDict()
902 #debug('buffers:%s \nlogs:%s' %(search_in_buffers, search_in_files))
903 time_start = now()
904
905 # buffers
906 if search_in_buffers:
907 regexp = make_regexp(pattern, matchcase)
908 for buffer in search_in_buffers:
909 buffer_name = weechat.buffer_get_string(buffer, 'name')
910 matched_lines[buffer_name] = grep_buffer(buffer, head, tail, after_context,
911 before_context, count, regexp, hilight, exact, invert)
912
913 # logs
914 if search_in_files:
915 size_limit = get_config_int('size_limit', allow_empty_string=True)
916 background = False
917 if size_limit or size_limit == 0:
918 size = sum(map(get_size, search_in_files))
919 if size > size_limit * 1024:
920 background = True
921 elif size_limit == '':
922 background = False
923
924 if not background:
925 # run grep normally
926 regexp = make_regexp(pattern, matchcase)
927 for log in search_in_files:
928 log_name = strip_home(log)
929 matched_lines[log_name] = grep_file(log, head, tail, after_context, before_context,
930 count, regexp, hilight, exact, invert)
931 buffer_update()
932 else:
933 # we hook a process so grepping runs in background.
934 #debug('on background')
935 global hook_file_grep, script_path, bytecode
936 timeout = 1000*60*5 # 5 min
937
938 quotify = lambda s: '"%s"' %s
939 files_string = ', '.join(map(quotify, search_in_files))
940
941 global tmpFile
942 # we keep the file descriptor as a global var so it isn't deleted until next grep
943 tmpFile = tempfile.NamedTemporaryFile(prefix=SCRIPT_NAME,
944 dir=weechat.info_get('weechat_dir', ''))
945 cmd = grep_process_cmd %dict(logs=files_string, head=head, pattern=pattern, tail=tail,
946 hilight=hilight, after_context=after_context, before_context=before_context,
947 exact=exact, matchcase=matchcase, home_dir=home_dir, script_path=script_path,
948 count=count, invert=invert, bytecode=bytecode, filename=tmpFile.name,
949 python=weechat.info_get('python2_bin', '') or 'python')
950
951 #debug(cmd)
952 hook_file_grep = weechat.hook_process(cmd, timeout, 'grep_file_callback', tmpFile.name)
953 global pattern_tmpl
954 if hook_file_grep:
955 buffer_create("Searching for '%s' in %s worth of data..." %(pattern_tmpl,
956 human_readable_size(size)))
957 else:
958 buffer_update()
959
960 # defined here for commodity
961 grep_process_cmd = """%(python)s -%(bytecode)sc '
962 import sys, cPickle, os
963 sys.path.append("%(script_path)s") # add WeeChat script dir so we can import grep
964 from grep import make_regexp, grep_file, strip_home
965 logs = (%(logs)s, )
966 try:
967 regexp = make_regexp("%(pattern)s", %(matchcase)s)
968 d = {}
969 for log in logs:
970 log_name = strip_home(log, "%(home_dir)s")
971 lines = grep_file(log, %(head)s, %(tail)s, %(after_context)s, %(before_context)s,
972 %(count)s, regexp, "%(hilight)s", %(exact)s, %(invert)s)
973 d[log_name] = lines
974 fd = open("%(filename)s", "wb")
975 cPickle.dump(d, fd, -1)
976 fd.close()
977 except Exception, e:
978 print >> sys.stderr, e'
979 """
980
981 grep_stdout = grep_stderr = ''
982 def grep_file_callback(filename, command, rc, stdout, stderr):
983 global hook_file_grep, grep_stderr, grep_stdout
984 global matched_lines
985 #debug("rc: %s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout)))
986 if stdout:
987 grep_stdout += stdout
988 if stderr:
989 grep_stderr += stderr
990 if int(rc) >= 0:
991
992 def set_buffer_error():
993 grep_buffer = buffer_create()
994 title = weechat.buffer_get_string(grep_buffer, 'title')
995 title = title + ' %serror' %color_title
996 weechat.buffer_set(grep_buffer, 'title', title)
997
998 try:
999 if grep_stderr:
1000 error(grep_stderr)
1001 set_buffer_error()
1002 #elif grep_stdout:
1003 #debug(grep_stdout)
1004 elif path.exists(filename):
1005 import cPickle
1006 try:
1007 #debug(file)
1008 fd = open(filename, 'rb')
1009 d = cPickle.load(fd)
1010 matched_lines.update(d)
1011 fd.close()
1012 except Exception, e:
1013 error(e)
1014 set_buffer_error()
1015 else:
1016 buffer_update()
1017 global tmpFile
1018 tmpFile = None
1019 finally:
1020 grep_stdout = grep_stderr = ''
1021 hook_file_grep = None
1022 return WEECHAT_RC_OK
1023
1024 def get_grep_file_status():
1025 global search_in_files, matched_lines, time_start
1026 elapsed = now() - time_start
1027 if len(search_in_files) == 1:
1028 log = '%s (%s)' %(strip_home(search_in_files[0]),
1029 human_readable_size(get_size(search_in_files[0])))
1030 else:
1031 size = sum(map(get_size, search_in_files))
1032 log = '%s log files (%s)' %(len(search_in_files), human_readable_size(size))
1033 return 'Searching in %s, running for %.4f seconds. Interrupt it with "/grep stop" or "stop"' \
1034 ' in grep buffer.' %(log, elapsed)
1035
1036 ### Grep buffer ###
1037 def buffer_update():
1038 """Updates our buffer with new lines."""
1039 global pattern_tmpl, matched_lines, pattern, count, hilight, invert, exact
1040 time_grep = now()
1041
1042 buffer = buffer_create()
1043 if get_config_boolean('clear_buffer'):
1044 weechat.buffer_clear(buffer)
1045 matched_lines.strip_separator() # remove first and last separators of each list
1046 len_total_lines = len(matched_lines)
1047 max_lines = get_config_int('max_lines')
1048 if not count and len_total_lines > max_lines:
1049 weechat.buffer_clear(buffer)
1050
1051 def _make_summary(log, lines, note):
1052 return '%s matches "%s%s%s"%s in %s%s%s%s' \
1053 %(lines.matches_count, color_summary, pattern_tmpl, color_info,
1054 invert and ' (inverted)' or '',
1055 color_summary, log, color_reset, note)
1056
1057 if count:
1058 make_summary = lambda log, lines : _make_summary(log, lines, ' (not shown)')
1059 else:
1060 def make_summary(log, lines):
1061 if lines.stripped_lines:
1062 if lines:
1063 note = ' (last %s lines shown)' %len(lines)
1064 else:
1065 note = ' (not shown)'
1066 else:
1067 note = ''
1068 return _make_summary(log, lines, note)
1069
1070 global weechat_format
1071 if hilight:
1072 # we don't want colors if there's match highlighting
1073 format_line = lambda s : '%s %s %s' %split_line(s)
1074 else:
1075 def format_line(s):
1076 global nick_dict, weechat_format
1077 date, nick, msg = split_line(s)
1078 if weechat_format:
1079 try:
1080 nick = nick_dict[nick]
1081 except KeyError:
1082 # cache nick
1083 nick_c = color_nick(nick)
1084 nick_dict[nick] = nick_c
1085 nick = nick_c
1086 return '%s%s %s%s %s' %(color_date, date, nick, color_reset, msg)
1087 else:
1088 #no formatting
1089 return msg
1090
1091 prnt(buffer, '\n')
1092 print_line('Search for "%s%s%s"%s in %s%s%s.' %(color_summary, pattern_tmpl, color_info,
1093 invert and ' (inverted)' or '', color_summary, matched_lines, color_reset),
1094 buffer)
1095 # print last <max_lines> lines
1096 if matched_lines.get_matches_count():
1097 if count:
1098 # with count we sort by matches lines instead of just lines.
1099 matched_lines_items = matched_lines.items_count()
1100 else:
1101 matched_lines_items = matched_lines.items()
1102
1103 matched_lines.get_last_lines(max_lines)
1104 for log, lines in matched_lines_items:
1105 if lines.matches_count:
1106 # matched lines
1107 if not count:
1108 # print lines
1109 weechat_format = True
1110 if exact:
1111 lines.onlyUniq()
1112 for line in lines:
1113 #debug(repr(line))
1114 if line == linesList._sep:
1115 # separator
1116 prnt(buffer, context_sep)
1117 else:
1118 if '\x00' in line:
1119 # log was corrupted
1120 error("Found garbage in log '%s', maybe it's corrupted" %log)
1121 line = line.replace('\x00', '')
1122 prnt_date_tags(buffer, 0, 'no_highlight', format_line(line))
1123
1124 # summary
1125 if count or get_config_boolean('show_summary'):
1126 summary = make_summary(log, lines)
1127 print_line(summary, buffer)
1128
1129 # separator
1130 if not count and lines:
1131 prnt(buffer, '\n')
1132 else:
1133 print_line('No matches found.', buffer)
1134
1135 # set title
1136 global time_start
1137 time_end = now()
1138 # total time
1139 time_total = time_end - time_start
1140 # percent of the total time used for grepping
1141 time_grep_pct = (time_grep - time_start)/time_total*100
1142 #debug('time: %.4f seconds (%.2f%%)' %(time_total, time_grep_pct))
1143 if not count and len_total_lines > max_lines:
1144 note = ' (last %s lines shown)' %len(matched_lines)
1145 else:
1146 note = ''
1147 title = "Search in %s%s%s %s matches%s | pattern \"%s%s%s\"%s %s | %.4f seconds (%.2f%%)" \
1148 %(color_title, matched_lines, color_reset, matched_lines.get_matches_count(), note,
1149 color_title, pattern_tmpl, color_reset, invert and ' (inverted)' or '', format_options(),
1150 time_total, time_grep_pct)
1151 weechat.buffer_set(buffer, 'title', title)
1152
1153 if get_config_boolean('go_to_buffer'):
1154 weechat.buffer_set(buffer, 'display', '1')
1155
1156 # free matched_lines so it can be removed from memory
1157 del matched_lines
1158
1159 def split_line(s):
1160 """Splits log's line 's' in 3 parts, date, nick and msg."""
1161 global weechat_format
1162 if weechat_format and s.count('\t') >= 2:
1163 date, nick, msg = s.split('\t', 2) # date, nick, message
1164 else:
1165 # looks like log isn't in weechat's format
1166 weechat_format = False # incoming lines won't be formatted
1167 date, nick, msg = '', '', s
1168 # remove tabs
1169 if '\t' in msg:
1170 msg = msg.replace('\t', ' ')
1171 return date, nick, msg
1172
1173 def print_line(s, buffer=None, display=False):
1174 """Prints 's' in script's buffer as 'script_nick'. For displaying search summaries."""
1175 if buffer is None:
1176 buffer = buffer_create()
1177 say('%s%s' %(color_info, s), buffer)
1178 if display and get_config_boolean('go_to_buffer'):
1179 weechat.buffer_set(buffer, 'display', '1')
1180
1181 def format_options():
1182 global matchcase, number, count, exact, hilight, invert
1183 global tail, head, after_context, before_context
1184 options = []
1185 append = options.append
1186 insert = options.insert
1187 chars = 'cHmov'
1188 for i, flag in enumerate((count, hilight, matchcase, exact, invert)):
1189 if flag:
1190 append(chars[i])
1191
1192 if head or tail:
1193 n = get_config_int('default_tail_head')
1194 if head:
1195 append('h')
1196 if head != n:
1197 insert(-1, ' -')
1198 append('n')
1199 append(head)
1200 elif tail:
1201 append('t')
1202 if tail != n:
1203 insert(-1, ' -')
1204 append('n')
1205 append(tail)
1206
1207 if before_context and after_context and (before_context == after_context):
1208 append(' -C')
1209 append(before_context)
1210 else:
1211 if before_context:
1212 append(' -B')
1213 append(before_context)
1214 if after_context:
1215 append(' -A')
1216 append(after_context)
1217
1218 s = ''.join(map(str, options)).strip()
1219 if s and s[0] != '-':
1220 s = '-' + s
1221 return s
1222
1223 def buffer_create(title=None):
1224 """Returns our buffer pointer, creates and cleans the buffer if needed."""
1225 buffer = weechat.buffer_search('python', SCRIPT_NAME)
1226 if not buffer:
1227 buffer = weechat.buffer_new(SCRIPT_NAME, 'buffer_input', '', '', '')
1228 weechat.buffer_set(buffer, 'time_for_each_line', '0')
1229 weechat.buffer_set(buffer, 'nicklist', '0')
1230 weechat.buffer_set(buffer, 'title', title or 'grep output buffer')
1231 weechat.buffer_set(buffer, 'localvar_set_no_log', '1')
1232 elif title:
1233 weechat.buffer_set(buffer, 'title', title)
1234 return buffer
1235
1236 def buffer_input(data, buffer, input_data):
1237 """Repeats last search with 'input_data' as regexp."""
1238 try:
1239 cmd_grep_stop(buffer, input_data)
1240 except:
1241 return WEECHAT_RC_OK
1242
1243 global search_in_buffers, search_in_files
1244 global pattern
1245 try:
1246 if pattern and (search_in_files or search_in_buffers):
1247 # check if the buffer pointers are still valid
1248 for pointer in search_in_buffers:
1249 infolist = weechat.infolist_get('buffer', pointer, '')
1250 if not infolist:
1251 del search_in_buffers[search_in_buffers.index(pointer)]
1252 weechat.infolist_free(infolist)
1253 try:
1254 cmd_grep_parsing(input_data)
1255 except Exception, e:
1256 error('Argument error, %s' %e, buffer=buffer)
1257 return WEECHAT_RC_OK
1258 try:
1259 show_matching_lines()
1260 except Exception, e:
1261 error(e)
1262 except NameError:
1263 error("There isn't any previous search to repeat.", buffer=buffer)
1264 return WEECHAT_RC_OK
1265
1266 ### Commands ###
1267 def cmd_init():
1268 """Resets global vars."""
1269 global home_dir, cache_dir, nick_dict
1270 global pattern_tmpl, pattern, matchcase, number, count, exact, hilight, invert
1271 global tail, head, after_context, before_context
1272 hilight = ''
1273 head = tail = after_context = before_context = invert = False
1274 matchcase = count = exact = False
1275 pattern_tmpl = pattern = number = None
1276 home_dir = get_home()
1277 cache_dir = {} # for avoid walking the dir tree more than once per command
1278 nick_dict = {} # nick cache for don't calculate nick color every time
1279
1280 def cmd_grep_parsing(args):
1281 """Parses args for /grep and grep input buffer."""
1282 global pattern_tmpl, pattern, matchcase, number, count, exact, hilight, invert
1283 global tail, head, after_context, before_context
1284 global log_name, buffer_name, only_buffers, all
1285 opts, args = getopt.gnu_getopt(args.split(), 'cmHeahtivn:bA:B:C:o', ['count', 'matchcase', 'hilight',
1286 'exact', 'all', 'head', 'tail', 'number=', 'buffer', 'after-context=', 'before-context=',
1287 'context=', 'invert', 'only-match'])
1288 #debug(opts, 'opts: '); debug(args, 'args: ')
1289 if len(args) >= 2:
1290 if args[0] == 'log':
1291 del args[0]
1292 log_name = args.pop(0)
1293 elif args[0] == 'buffer':
1294 del args[0]
1295 buffer_name = args.pop(0)
1296
1297 def tmplReplacer(match):
1298 """This function will replace templates with regexps"""
1299 s = match.groups()[0]
1300 tmpl_args = s.split()
1301 tmpl_key, _, tmpl_args = s.partition(' ')
1302 try:
1303 template = templates[tmpl_key]
1304 if callable(template):
1305 r = template(tmpl_args)
1306 if not r:
1307 error("Template %s returned empty string "\
1308 "(WeeChat doesn't have enough data)." %t)
1309 return r
1310 else:
1311 return template
1312 except:
1313 return t
1314
1315 args = ' '.join(args) # join pattern for keep spaces
1316 if args:
1317 pattern_tmpl = args
1318 pattern = _tmplRe.sub(tmplReplacer, args)
1319 debug('Using regexp: %s', pattern)
1320 if not pattern:
1321 raise Exception, 'No pattern for grep the logs.'
1322
1323 def positive_number(opt, val):
1324 try:
1325 number = int(val)
1326 if number < 0:
1327 raise ValueError
1328 return number
1329 except ValueError:
1330 if len(opt) == 1:
1331 opt = '-' + opt
1332 else:
1333 opt = '--' + opt
1334 raise Exception, "argument for %s must be a positive integer." %opt
1335
1336 for opt, val in opts:
1337 opt = opt.strip('-')
1338 if opt in ('c', 'count'):
1339 count = not count
1340 elif opt in ('m', 'matchcase'):
1341 matchcase = not matchcase
1342 elif opt in ('H', 'hilight'):
1343 # hilight must be always a string!
1344 if hilight:
1345 hilight = ''
1346 else:
1347 hilight = '%s,%s' %(color_hilight, color_reset)
1348 # we pass the colors in the variable itself because check_string() must not use
1349 # weechat's module when applying the colors (this is for grep in a hooked process)
1350 elif opt in ('e', 'exact', 'o', 'only-match'):
1351 exact = not exact
1352 invert = False
1353 elif opt in ('a', 'all'):
1354 all = not all
1355 elif opt in ('h', 'head'):
1356 head = not head
1357 tail = False
1358 elif opt in ('t', 'tail'):
1359 tail = not tail
1360 head = False
1361 elif opt in ('b', 'buffer'):
1362 only_buffers = True
1363 elif opt in ('n', 'number'):
1364 number = positive_number(opt, val)
1365 elif opt in ('C', 'context'):
1366 n = positive_number(opt, val)
1367 after_context = n
1368 before_context = n
1369 elif opt in ('A', 'after-context'):
1370 after_context = positive_number(opt, val)
1371 elif opt in ('B', 'before-context'):
1372 before_context = positive_number(opt, val)
1373 elif opt in ('i', 'v', 'invert'):
1374 invert = not invert
1375 exact = False
1376 # number check
1377 if number is not None:
1378 if number == 0:
1379 head = tail = False
1380 number = None
1381 elif head:
1382 head = number
1383 elif tail:
1384 tail = number
1385 else:
1386 n = get_config_int('default_tail_head')
1387 if head:
1388 head = n
1389 elif tail:
1390 tail = n
1391
1392 def cmd_grep_stop(buffer, args):
1393 global hook_file_grep, pattern, matched_lines, tmpFile
1394 if hook_file_grep:
1395 if args == 'stop':
1396 weechat.unhook(hook_file_grep)
1397 hook_file_grep = None
1398 s = 'Search for \'%s\' stopped.' %pattern
1399 say(s, buffer)
1400 grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
1401 if grep_buffer:
1402 weechat.buffer_set(grep_buffer, 'title', s)
1403 del matched_lines
1404 tmpFile = None
1405 else:
1406 say(get_grep_file_status(), buffer)
1407 raise Exception
1408
1409 def cmd_grep(data, buffer, args):
1410 """Search in buffers and logs."""
1411 global pattern, matchcase, head, tail, number, count, exact, hilight
1412 try:
1413 cmd_grep_stop(buffer, args)
1414 except:
1415 return WEECHAT_RC_OK
1416
1417 if not args:
1418 weechat.command('', '/help %s' %SCRIPT_COMMAND)
1419 return WEECHAT_RC_OK
1420
1421 cmd_init()
1422 global log_name, buffer_name, only_buffers, all
1423 log_name = buffer_name = ''
1424 only_buffers = all = False
1425
1426 # parse
1427 try:
1428 cmd_grep_parsing(args)
1429 except Exception, e:
1430 error('Argument error, %s' %e)
1431 return WEECHAT_RC_OK
1432
1433 # find logs
1434 log_file = search_buffer = None
1435 if log_name:
1436 log_file = get_file_by_pattern(log_name, all)
1437 if not log_file:
1438 error("Couldn't find any log for %s. Try /logs" %log_name)
1439 return WEECHAT_RC_OK
1440 elif all:
1441 search_buffer = get_all_buffers()
1442 elif buffer_name:
1443 search_buffer = get_buffer_by_name(buffer_name)
1444 if not search_buffer:
1445 # there's no buffer, try in the logs
1446 log_file = get_file_by_name(buffer_name)
1447 if not log_file:
1448 error("Logs or buffer for '%s' not found." %buffer_name)
1449 return WEECHAT_RC_OK
1450 else:
1451 search_buffer = [search_buffer]
1452 else:
1453 search_buffer = [buffer]
1454
1455 # make the log list
1456 global search_in_files, search_in_buffers
1457 search_in_files = []
1458 search_in_buffers = []
1459 if log_file:
1460 search_in_files = log_file
1461 elif not only_buffers:
1462 #debug(search_buffer)
1463 for pointer in search_buffer:
1464 log = get_file_by_buffer(pointer)
1465 #debug('buffer %s log %s' %(pointer, log))
1466 if log:
1467 search_in_files.append(log)
1468 else:
1469 search_in_buffers.append(pointer)
1470 else:
1471 search_in_buffers = search_buffer
1472
1473 # grepping
1474 try:
1475 show_matching_lines()
1476 except Exception, e:
1477 error(e)
1478 return WEECHAT_RC_OK
1479
1480 def cmd_logs(data, buffer, args):
1481 """List files in Weechat's log dir."""
1482 cmd_init()
1483 global home_dir
1484 sort_by_size = False
1485 filter = []
1486
1487 try:
1488 opts, args = getopt.gnu_getopt(args.split(), 's', ['size'])
1489 if args:
1490 filter = args
1491 for opt, var in opts:
1492 opt = opt.strip('-')
1493 if opt in ('size', 's'):
1494 sort_by_size = True
1495 except Exception, e:
1496 error('Argument error, %s' %e)
1497 return WEECHAT_RC_OK
1498
1499 # is there's a filter, filter_excludes should be False
1500 file_list = dir_list(home_dir, filter, filter_excludes=not filter)
1501 if sort_by_size:
1502 file_list.sort(key=get_size)
1503 else:
1504 file_list.sort()
1505
1506 file_sizes = map(lambda x: human_readable_size(get_size(x)), file_list)
1507 # calculate column lenght
1508 if file_list:
1509 L = file_list[:]
1510 L.sort(key=len)
1511 bigest = L[-1]
1512 column_len = len(bigest) + 3
1513 else:
1514 column_len = ''
1515
1516 buffer = buffer_create()
1517 if get_config_boolean('clear_buffer'):
1518 weechat.buffer_clear(buffer)
1519 file_list = zip(file_list, file_sizes)
1520 msg = 'Found %s logs.' %len(file_list)
1521
1522 print_line(msg, buffer, display=True)
1523 for file, size in file_list:
1524 separator = column_len and '.'*(column_len - len(file))
1525 prnt(buffer, '%s %s %s' %(strip_home(file), separator, size))
1526 if file_list:
1527 print_line(msg, buffer)
1528 return WEECHAT_RC_OK
1529
1530
1531 ### Completion ###
1532 def completion_log_files(data, completion_item, buffer, completion):
1533 #debug('completion: %s' %', '.join((data, completion_item, buffer, completion)))
1534 global home_dir
1535 l = len(home_dir)
1536 completion_list_add = weechat.hook_completion_list_add
1537 WEECHAT_LIST_POS_END = weechat.WEECHAT_LIST_POS_END
1538 for log in dir_list(home_dir):
1539 completion_list_add(completion, log[l:], 0, WEECHAT_LIST_POS_END)
1540 return WEECHAT_RC_OK
1541
1542 def completion_grep_args(data, completion_item, buffer, completion):
1543 for arg in ('count', 'all', 'matchcase', 'hilight', 'exact', 'head', 'tail', 'number', 'buffer',
1544 'after-context', 'before-context', 'context', 'invert', 'only-match'):
1545 weechat.hook_completion_list_add(completion, '--' + arg, 0, weechat.WEECHAT_LIST_POS_SORT)
1546 for tmpl in templates:
1547 weechat.hook_completion_list_add(completion, '%{' + tmpl, 0, weechat.WEECHAT_LIST_POS_SORT)
1548 return WEECHAT_RC_OK
1549
1550
1551 ### Templates ###
1552 # template placeholder
1553 _tmplRe = re.compile(r'%\{(\w+.*?)(?:\}|$)')
1554 # will match 999.999.999.999 but I don't care
1555 ipAddress = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
1556 domain = r'[\w-]{2,}(?:\.[\w-]{2,})*\.[a-z]{2,}'
1557 url = r'\w+://(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?' % (domain, ipAddress)
1558
1559 def make_url_regexp(args):
1560 #debug('make url: %s', args)
1561 if args:
1562 words = r'(?:%s)' %'|'.join(map(re.escape, args.split()))
1563 return r'(?:\w+://|www\.)[^\s]*%s[^\s]*(?:/[^\])>\s]*)?' %words
1564 else:
1565 return url
1566
1567 def make_simple_regexp(pattern):
1568 s = ''
1569 for c in pattern:
1570 if c == '*':
1571 s += '.*'
1572 elif c == '?':
1573 s += '.'
1574 else:
1575 s += re.escape(c)
1576 return s
1577
1578 templates = {
1579 'ip': ipAddress,
1580 'url': make_url_regexp,
1581 'escape': lambda s: re.escape(s),
1582 'simple': make_simple_regexp,
1583 'domain': domain,
1584 }
1585
1586 ### Main ###
1587 def delete_bytecode():
1588 global script_path
1589 bytecode = path.join(script_path, SCRIPT_NAME + '.pyc')
1590 if path.isfile(bytecode):
1591 os.remove(bytecode)
1592 return WEECHAT_RC_OK
1593
1594 if __name__ == '__main__' and import_ok and \
1595 weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, \
1596 SCRIPT_DESC, 'delete_bytecode', ''):
1597 home_dir = get_home()
1598
1599 # for import ourselves
1600 global script_path
1601 script_path = path.dirname(__file__)
1602 sys.path.append(script_path)
1603 delete_bytecode()
1604
1605 # check python version
1606 import sys
1607 global bytecode
1608 if sys.version_info > (2, 6):
1609 bytecode = 'B'
1610 else:
1611 bytecode = ''
1612
1613
1614 weechat.hook_command(SCRIPT_COMMAND, cmd_grep.__doc__,
1615 "[log <file> | buffer <name> | stop] [-a|--all] [-b|--buffer] [-c|--count] [-m|--matchcase] "
1616 "[-H|--hilight] [-o|--only-match] [-i|-v|--invert] [(-h|--head)|(-t|--tail) [-n|--number <n>]] "
1617 "[-A|--after-context <n>] [-B|--before-context <n>] [-C|--context <n> ] <expression>",
1618 # help
1619 """
1620 log <file>: Search in one log that matches <file> in the logger path.
1621 Use '*' and '?' as wildcards.
1622 buffer <name>: Search in buffer <name>, if there's no buffer with <name> it will
1623 try to search for a log file.
1624 stop: Stops a currently running search.
1625 -a --all: Search in all open buffers.
1626 If used with 'log <file>' search in all logs that matches <file>.
1627 -b --buffer: Search only in buffers, not in file logs.
1628 -c --count: Just count the number of matched lines instead of showing them.
1629 -m --matchcase: Don't do case insensible search.
1630 -H --hilight: Colour exact matches in output buffer.
1631 -o --only-match: Print only the matching part of the line (unique matches).
1632 -v -i --invert: Print lines that don't match the regular expression.
1633 -t --tail: Print the last 10 matching lines.
1634 -h --head: Print the first 10 matching lines.
1635 -n --number <n>: Overrides default number of lines for --tail or --head.
1636 -A --after-context <n>: Shows <n> lines of trailing context after matching lines.
1637 -B --before-context <n>: Shows <n> lines of leading context before matching lines.
1638 -C --context <n>: Same as using both --after-context and --before-context simultaneously.
1639 <expression>: Expression to search.
1640
1641 Grep buffer:
1642 Input line accepts most arguments of /grep, it'll repeat last search using the new
1643 arguments provided. You can't search in different logs from the buffer's input.
1644 Boolean arguments like --count, --tail, --head, --hilight, ... are toggleable
1645
1646 Python regular expression syntax:
1647 See http://docs.python.org/lib/re-syntax.html
1648
1649 Grep Templates:
1650 %{url [text]}: Matches anything like an url, or an url with text.
1651 %{ip}: Matches anything that looks like an ip.
1652 %{domain}: Matches anything like a domain.
1653 %{escape text}: Escapes text in pattern.
1654 %{simple pattern}: Converts a pattern with '*' and '?' wildcards into a regexp.
1655
1656 Examples:
1657 Search for urls with the word 'weechat' said by 'nick'
1658 /grep nick\\t.*%{url weechat}
1659 Search for '*.*' string
1660 /grep %{escape *.*}
1661 """,
1662 # completion template
1663 "buffer %(buffers_names) %(grep_arguments)|%*"
1664 "||log %(grep_log_files) %(grep_arguments)|%*"
1665 "||stop"
1666 "||%(grep_arguments)|%*",
1667 'cmd_grep' ,'')
1668 weechat.hook_command('logs', cmd_logs.__doc__, "[-s|--size] [<filter>]",
1669 "-s --size: Sort logs by size.\n"
1670 " <filter>: Only show logs that match <filter>. Use '*' and '?' as wildcards.", '--size', 'cmd_logs', '')
1671
1672 weechat.hook_completion('grep_log_files', "list of log files",
1673 'completion_log_files', '')
1674 weechat.hook_completion('grep_arguments', "list of arguments",
1675 'completion_grep_args', '')
1676
1677 # settings
1678 for opt, val in settings.iteritems():
1679 if not weechat.config_is_set_plugin(opt):
1680 weechat.config_set_plugin(opt, val)
1681
1682 # colors
1683 color_date = weechat.color('brown')
1684 color_info = weechat.color('cyan')
1685 color_hilight = weechat.color('lightred')
1686 color_reset = weechat.color('reset')
1687 color_title = weechat.color('yellow')
1688 color_summary = weechat.color('lightcyan')
1689 color_delimiter = weechat.color('chat_delimiters')
1690 color_script_nick = weechat.color('chat_nick')
1691
1692 # pretty [grep]
1693 script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_script_nick, SCRIPT_NAME, color_delimiter,
1694 color_reset)
1695 script_nick_nocolor = '[%s]' %SCRIPT_NAME
1696 # paragraph separator when using context options
1697 context_sep = '%s\t%s--' %(script_nick, color_info)
1698
1699 # -------------------------------------------------------------------------
1700 # Debug
1701
1702 if weechat.config_get_plugin('debug'):
1703 try:
1704 # custom debug module I use, allows me to inspect script's objects.
1705 import pybuffer
1706 debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME)
1707 except:
1708 def debug(s, *args):
1709 if not isinstance(s, basestring):
1710 s = str(s)
1711 if args:
1712 s = s %args
1713 prnt('', '%s\t%s' %(script_nick, s))
1714 else:
1715 def debug(*args):
1716 pass
1717
1718 # vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: