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