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