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