new-words

view new-words.py @ 68:846240941452

added -C key: compress to lines; fixed bug with #90-line
author Igor Chubin <igor@chub.in>
date Sun Sep 23 16:07:29 2012 +0300 (2012-09-23)
parents 5a003076eb11
children
line source
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 from __future__ import with_statement
5 import codecs
6 import difflib
7 import logging
8 import os
9 import optparse
10 import re
11 import subprocess
12 import sys
13 import Stemmer
14 import tempfile
15 try:
16 import psyco
17 psyco.full()
18 except:
19 pass
21 config = {
22 'config_directory': os.environ['HOME'] + '/.new-words',
23 'language': 'en',
24 }
26 logging.basicConfig(filename='/tmp/new-words-py.log', level=logging.DEBUG)
28 class Normalizator:
29 def __init__(self, language, linked_words={}):
30 stemmer_algorithm = {
31 'de' : 'german',
32 'fr' : 'french',
33 'en' : 'english',
34 'es' : 'spanish',
35 'ru' : 'russian',
36 'it' : 'italian',
37 'uk' : 'ukrainian',
38 }
39 try:
40 self.stemmer = Stemmer.Stemmer(stemmer_algorithm[language])
41 except:
42 self.stemmer = None
43 self.linked_words = linked_words
45 def normalize(self, word):
46 word_chain = []
47 while word in self.linked_words and not word in word_chain:
48 word_chain.append(word)
49 word = self.linked_words[word]
50 if self.stemmer:
51 return self.stemmer.stemWord(word.lower())
52 else:
53 return word.lower()
55 def best_word_from_group(self, wordpairs_group):
56 """Returns the word that is the most relevant to the wordpairs_group.
58 At the moment: returns the word with minimal length"""
60 def f(x, y):
61 return difflib.SequenceMatcher(
62 None,
63 #(x[-2:] == 'en' and x[:-2].lower() or x.lower()),
64 x.lower(),
65 y.lower()).ratio()
67 minimal_length = min(len(pair[1]) for pair in wordpairs_group)
68 best_match = list(x[1] for x in sorted(
69 (x for x in wordpairs_group if len(x[1]) == minimal_length),
70 key=lambda x:x[0],
71 reverse=True))[0]
73 return best_match
75 suggestions = self.dictionary_suggestions(best_match)
76 if len(suggestions) == 1:
77 return best_match
79 verb = False
80 corrected_best_match = best_match
81 if best_match[-2:] == 'et':
82 word = best_match[:-1]+"n"
83 sugg = self.dictionary_suggestions(word)
84 if len(sugg) == 1:
85 return word
86 suggestions += sugg
87 corrected_best_match = word
88 corrected_best_match = best_match[:-2]
89 verb = True
91 if best_match[-1] == 't':
92 word = best_match[:-1]+"en"
93 sugg = self.dictionary_suggestions(word)
94 if len(sugg) == 1:
95 return word
96 suggestions += sugg
97 corrected_best_match = best_match[:-1]
98 verb = True
100 if corrected_best_match[0].lower() == corrected_best_match[0]:
101 suggestions = [ x for x in suggestions
102 if x[0].lower() == x[0] ]
104 if suggestions == []:
105 return best_match+"_"
106 return best_match+" "+(" ".join(
107 sorted(
108 suggestions,
109 key = lambda x: f(x, corrected_best_match),
110 reverse = True
111 )
112 )
113 )
115 def dictionary_suggestions(self, word):
116 return [
117 x.decode('utf-8').rstrip('\n')
118 for x
119 in subprocess.Popen(
120 ["de-variants", word],
121 stdout=subprocess.PIPE
122 ).stdout.readlines() ]
125 parser = optparse.OptionParser()
127 parser.add_option(
128 "-a", "--no-marks",
129 help="don't add marks (and don't save marks added by user) [NOT IMPLEMENTED YET]",
130 action="store_true",
131 dest="no_marks")
133 parser.add_option(
134 "-c", "--compressed",
135 help="show compressed wordlist: one word per group",
136 action="store_true",
137 dest="compressed")
139 parser.add_option(
140 "-C", "--compressed-to-line",
141 help="show compressed wordlist: all words of the group in a line",
142 action="store_true",
143 dest="compressed_to_line")
145 parser.add_option(
146 "-k", "--known-words",
147 help="put higher words that are similar to the known words (only for English)",
148 action="store_true",
149 dest="compressed")
151 parser.add_option(
152 "-l", "--language",
153 help="specify language of text",
154 action="store",
155 dest="language")
157 parser.add_option(
158 "-f", "--allowed-words",
159 help="file with list of allowed words (words that will be shown in the output)",
160 action="store",
161 dest="allowed_words")
163 parser.add_option(
164 "-G", "--words-grouping",
165 help="turn off word grouping",
166 action="store_true",
167 dest="no_words_grouping")
169 parser.add_option(
170 "-X", "--function",
171 help="filter through subsystem [INTERNAL]",
172 action="store",
173 dest="function")
175 parser.add_option(
176 "-m", "--merge-tag",
177 help="merge words tagged with specified tag into the main vocabulary [NOT IMPLEMENTED YET]",
178 action="store",
179 dest="merge_tag")
181 parser.add_option(
182 "-M", "--merge-tagged",
183 help="merge words tagged with ANY tag into the main vocabulary [NOT IMPLEMENTED YET]",
184 action="store_true",
185 dest="merge_tagged")
187 parser.add_option(
188 "-n", "--non-interactive",
189 help="non-interactive mode (don't run vi)",
190 action="store_true",
191 dest="non_interactive")
193 parser.add_option(
194 "-N", "--no-filter",
195 help="switch off known words filtering",
196 action="store_true",
197 dest="no_filter")
199 parser.add_option(
200 "-p", "--pages",
201 help="work with specified pages only (pages = start-stop/total )",
202 action="store",
203 dest="pages")
205 parser.add_option(
206 "-d", "--delete-tag",
207 help="delete subvocabulary of specified tag",
208 action="store",
209 dest="delete_tag")
211 parser.add_option(
212 "-r", "--show-range",
213 help="show only words specified number of words",
214 action="store",
215 dest="show_range")
217 parser.add_option(
218 "-R", "--show-range-percentage",
219 help="show only words that cover specified percentage of the text, skip the rest",
220 action="store",
221 dest="show_range_percentage")
223 parser.add_option(
224 "-s", "--text-stats",
225 help="show the text statistics (percentage of known words and so on) and exit",
226 action="store_true",
227 dest="text_stats")
229 parser.add_option(
230 "-S", "--voc-stats",
231 help="show your vocabulary statistics (number of words and word groups) [NOT IMPLEMENTED YET]",
232 action="store_true",
233 dest="voc_stats")
235 parser.add_option(
236 "-t", "--tag",
237 help="tag known words with tag",
238 action="store",
239 dest="tag")
241 parser.add_option(
242 "-T", "--show-tags",
243 help="tag known words with tag",
244 action="store_true",
245 dest="show_tags")
247 parser.add_option(
248 "-v", "--vocabulary-filename",
249 help="use specified file as a vocabulary",
250 action="store",
251 dest="vocabulary_filename")
253 parser.add_option(
254 "-w", "--web",
255 help="Web browser version",
256 action="store_true",
257 dest="web")
259 parser.add_option(
260 "-2", "--two-words",
261 help="find 2 words' sequences",
262 action="store_true",
263 dest="two_words")
265 parser.add_option(
266 "-3", "--three-words",
267 help="find 3 words' sequences",
268 action="store_true",
269 dest="three_words")
271 def readlines_from_file(filename):
272 res = []
273 with codecs.open(filename, "r", "utf-8") as f:
274 for line in f.readlines():
275 res += [line]
276 return res
278 def readlines_from_url(url):
279 return [x.decode('utf-8') for x in
280 subprocess.Popen(
281 "lynx -dump '{url}' | perl -p -e 's@http://[a-zA-Z&_.:/0-9%?=,#+()\[\]~-]*@@'".format(url=url),
282 shell = True,
283 stdout = subprocess.PIPE,
284 stderr = subprocess.STDOUT
285 ).communicate()[0].split('\n')
286 ]
288 def readlines_from_stdin():
289 return codecs.getreader("utf-8")(sys.stdin).readlines()
291 def words_from_line(line):
292 line = line.rstrip('\n')
293 #return re.split('(?:\s|[*\r,.:#@()+=<>$;"?!|\[\]^%&~{}«»–])+', line)
294 #return re.split('[^a-zA-ZäöëüßÄËÖÜß]+', line)
295 return re.compile("(?!['_])(?:\W)+", flags=re.UNICODE).split(line)
297 def get_words(lines, group_by=[1]):
298 """
299 Returns hash of words in a file
300 word => number
301 """
302 result = {}
303 (a, b, c) = ("", "", "")
304 for line in lines:
305 words = words_from_line(line)
306 for word in words:
307 if re.match('[0-9]*$', word):
308 continue
309 result.setdefault(word, 0)
310 result[word] += 1
311 if 2 in group_by and a != "" and b != "":
312 w = "%s_%s" % (a,b)
313 result.setdefault(w, 0)
314 result[w] += 1
315 if 3 in group_by and not "" in [a,b,c]:
316 w = "%s_%s_%s" % (a,b,c)
317 result.setdefault(w, 0)
318 result[w] += 1
319 (a,b,c) = (b, c, word)
321 logging.debug(result)
322 return result
324 def voc_filename():
325 if 'vocabulary_filename' in config:
326 return config['vocabulary_filename']
327 return "%s/%s.txt"%(config['config_directory'], config['language'])
329 def load_vocabulary():
330 return get_words(readlines_from_file(voc_filename()))
332 def notes_filenames():
333 return ["%s/notes-%s.txt"%(config['config_directory'], config['language'])]
335 def load_notes(files):
336 notes = {}
337 for filename in files:
338 with codecs.open(filename, "r", "utf-8") as f:
339 for line in f.readlines():
340 (word, note) = re.split('\s+', line.rstrip('\n'), maxsplit=1)
341 notes.setdefault(word, {})
342 notes[word][filename] = note
343 return notes
345 def add_notes(lines, notes):
346 notes_filename = notes_filenames()[0]
347 result = []
348 for line in lines:
349 if line.startswith('#'):
350 result += [line]
351 else:
352 match_object = re.search('^\s*\S+\s*(\S+)', line)
353 if match_object:
354 word = match_object.group(1)
355 if word in notes:
356 if notes_filename in notes[word]:
357 line = line.rstrip('\n')
358 line = "%-30s %s\n" % (line, notes[word][notes_filename])
359 result += [line]
360 else:
361 result += [line]
362 else:
363 result += [line]
364 return result
366 def remove_notes(lines, notes_group):
367 notes_filename = notes_filenames()[0]
368 notes = {}
369 for k in notes_group.keys():
370 if notes_filename in notes_group[k]:
371 notes[k] = notes_group[k][notes_filename]
373 result = []
374 for line in lines:
375 line = line.rstrip('\n')
376 match_object = re.match('(\s+)(\S+)(\s+)(\S+)(\s+)(.*)', line)
377 if match_object:
378 result.append("".join([
379 match_object.group(1),
380 match_object.group(2),
381 match_object.group(3),
382 match_object.group(4),
383 "\n"
384 ]))
385 notes[match_object.group(4)] = match_object.group(6)
386 else:
387 result.append(line+"\n")
389 save_notes(notes_filename, notes)
390 return result
392 def save_notes(filename, notes):
393 lines = []
394 saved_words = []
395 with codecs.open(filename, "r", "utf-8") as f:
396 for line in f.readlines():
397 (word, note) = re.split('\s+', line.rstrip('\n'), maxsplit=1)
398 if word in notes:
399 line = "%-29s %s\n" % (word, notes[word])
400 saved_words.append(word)
401 lines.append(line)
402 for word in [x for x in notes.keys() if not x in saved_words]:
403 line = "%-29s %s\n" % (word, notes[word])
404 lines.append(line)
406 with codecs.open(filename, "w", "utf-8") as f:
407 for line in lines:
408 f.write(line)
411 def substract_dictionary(dict1, dict2):
412 """
413 returns dict1 - dict2
414 """
415 result = {}
416 for (k,v) in dict1.items():
417 if not k in dict2:
418 result[k] = v
419 return result
421 def dump_words(words, filename):
422 with codecs.open(filename, "w+", "utf-8") as f:
423 for word in words.keys():
424 f.write(("%s\n"%word)*words[word])
426 def error_message(text):
427 print text
429 def find_wordgroups_weights(word_pairs, normalizator):
430 weight = {}
431 for (num, word) in word_pairs:
432 normalized = normalizator.normalize(word)
433 weight.setdefault(normalized, 0)
434 weight[normalized] += num
435 return weight
437 def find_linked_words(notes):
438 linked_words = {}
439 for word in notes.keys():
440 for note in notes[word].values():
441 if "@" in note:
442 result = re.search(r'\@(\S*)', note)
443 if result:
444 main_word = result.group(1)
445 if main_word:
446 linked_words[word] = main_word
447 return linked_words
449 def compare_word_pairs(pair1, pair2, wgw, normalizator, linked_words):
450 (num1, word1) = pair1
451 (num2, word2) = pair2
453 normalized_word1 = normalizator.normalize(word1)
454 normalized_word2 = normalizator.normalize(word2)
456 cmp_res = cmp(wgw[normalized_word1], wgw[normalized_word2])
457 if cmp_res != 0:
458 return cmp_res
459 else:
460 cmp_res = cmp(normalized_word1, normalized_word2)
461 if cmp_res != 0:
462 return cmp_res
463 else:
464 return cmp(int(num1), int(num2))
467 def print_words_sorted(
468 word_pairs,
469 stats,
470 normalizator,
471 print_stats=True,
472 stats_only=False,
473 compressed_wordlist=False,
474 compressed_to_line = False,
475 show_range=0,
476 show_range_percentage=0,
477 ):
478 result = []
479 if stats_only:
480 #codecs.getwriter("utf-8")(sys.stdout).write(
481 result.append(
482 " ".join([
483 "%-10s" % x for x in [
484 "LANG",
485 "KNOWN%",
486 "UNKNOWN%",
487 "KNOWN",
488 "TOTAL",
489 "WPS",
490 "UWPS*10"
491 ]]) + "\n")
492 result.append(
493 " ".join([
494 "%(language)-10s",
495 "%(percentage)-10.2f",
496 "%(percentage_unknown)-10.2f",
497 "%(total_known)-11d"
498 "%(total)-11d"
499 "%(wps)-11d"
500 "%(uwps)-11d"
501 ]) % stats + "\n")
502 return "".join(result)
504 if print_stats:
505 result.append(
506 "# %(language)s, %(percentage)-7.2f, <%(total_known)s/%(total)s>, <%(groups)s/%(words)s>\n" % stats)
508 known = int(stats['total_known'])
509 total = int(stats['total'])
510 level_lines = range(int(float(stats['percentage']))/5*5+5,90,5)+range(90,102)
511 if 100.0*known/total >= level_lines[0]:
512 current_level = level_lines[0]
513 while 100.0*known/total > level_lines[0]:
514 current_level = level_lines[0]
515 level_lines = level_lines[1:]
516 old_normalized_word = None
517 words_of_this_group = []
518 printed_words = 0
519 for word_pair in word_pairs:
521 normalized_word = normalizator.normalize(word_pair[1])
522 if old_normalized_word and old_normalized_word != normalized_word:
523 if compressed_wordlist:
524 compressed_word_pair = (
525 sum(x[0] for x in words_of_this_group),
526 normalizator.best_word_from_group(words_of_this_group)
527 )
528 if compressed_to_line:
529 result.append("%10s %s %s\n" % (compressed_word_pair + (" ".join(y for x,y in words_of_this_group if y not in compressed_word_pair),)))
530 else:
531 result.append("%10s %s\n" % compressed_word_pair)
532 printed_words += 1
533 words_of_this_group = []
535 old_normalized_word = normalized_word
536 words_of_this_group.append(word_pair)
538 if not compressed_wordlist:
539 result.append("%10s %s\n" % word_pair)
540 printed_words += 1
543 known += word_pair[0]
544 if 100.0*known/total >= level_lines[0]:
545 current_level = level_lines[0]
546 while 100.0*known/total > level_lines[0]:
547 current_level = level_lines[0]
548 level_lines = level_lines[1:]
549 result.append("# %s\n" % current_level)
551 if show_range >0 and printed_words >= show_range:
552 break
553 if show_range_percentage >0 and 100.0*known/total >= show_range_percentage:
554 break
556 return result
558 def parse_parts_description(parts_description):
559 """
560 Returns triad (start, stop, step)
561 basing on parts_description string.
562 from-to/step
563 from+delta/step
564 """
566 try:
567 (a, step) = parts_description.split("/", 1)
568 step = int(step)
569 start = 0
570 stop = 0
571 if '-' in a:
572 (start, stop) = a.split("-", 1)
573 start = int(start)
574 stop = int(stop)
575 elif '+' in a:
576 (start, stop) = a.split("+", 1)
577 start = int(start)
578 stop = int(stop)
579 else:
580 start = int(a)
581 stop = start + 1
582 return (start, stop, step)
584 except:
585 raise ValueError("Parts description must be in format: num[[+-]num]/num; this [%s] is incorrect" % parts_description)
588 def take_part(lines, part_description = None):
589 if part_description == None or part_description == '':
590 return lines
591 (start, stop, step) = parse_parts_description(part_description)
592 n = len(lines)
593 part_size = (1.0*n) / step
594 result = []
595 for i in range(n):
596 if i >= start * part_size and i <= stop * part_size:
597 result += [lines[i]]
598 return result
600 def web_editor(output):
601 from twisted.internet import reactor
602 from twisted.web.server import Site
603 from twisted.web.static import File
604 from twisted.web.resource import Resource
605 import json
607 word_list = []
609 for o in output:
610 a = re.split('\s+', o.strip(), 2)
611 a = a + ['']*(3-len(a))
612 word_list.append({'number':a[0], 'word':a[1], 'comment':a[2]})
614 print "Loaded ", len(word_list)
616 new_words_html = "/home/igor/hg/new-words/web"
618 class JSONPage(Resource):
619 isLeaf = True
620 def render_GET(self, request):
621 return json.dumps({"word_list": word_list})
623 class SaveJSON(Resource):
624 isLeaf = True
625 def render_POST(self, request):
626 print json.loads(request.args["selected_words"][0])
627 return json.dumps({"status": "ok"})
629 json_page = JSONPage()
630 save_json = SaveJSON()
632 resource = File(new_words_html)
633 resource.putChild("json", json_page)
634 resource.putChild("save", save_json)
636 factory = Site(resource)
637 reactor.listenTCP(8880, factory)
638 reactor.run()
641 def filter_get_words_group_words_add_stat(args):
642 vocabulary = load_vocabulary()
643 notes = load_notes(notes_filenames())
645 input_lines = []
646 if len(args) > 0:
647 for arg in args:
648 if 'http://' in arg:
649 input_lines += readlines_from_url(arg)
650 else:
651 input_lines += readlines_from_file(arg)
652 else:
653 input_lines += readlines_from_stdin()
655 if len(input_lines) == 0:
656 print >> sys.stderr, "Nothing to do, standard input is empty, exiting."
657 sys.exit(1)
659 lines = take_part(input_lines, config.get('pages', ''))
661 (_, original_text_tempfile) = tempfile.mkstemp(prefix='new-word')
662 with codecs.open(original_text_tempfile, "w", "utf-8") as f:
663 f.write("".join(lines))
665 group_by = [1]
667 if 'two_words' in config:
668 group_by.append(2)
669 if 'three_words' in config:
670 group_by.append(3)
671 words = get_words(lines, group_by)
672 stats_only = False
673 if 'text_stats' in config:
674 stats_only = True
676 compressed_wordlist = False
677 if 'compressed' in config or 'compressed_to_line' in config:
678 compressed_wordlist = True
680 compressed_to_line = 'compressed_to_line' in config
682 if 'show_range' in config:
683 show_range = int(config['show_range'])
684 else:
685 show_range = 0
687 if 'show_range_percentage' in config:
688 show_range_percentage = int(config['show_range_percentage'])
689 else:
690 show_range_percentage = 0
693 stats = {}
694 stats['total'] = sum(words[x] for x in words.keys())
695 if not 'no_filter' in config:
696 words = substract_dictionary(words, vocabulary)
698 stats['total_unknown'] = sum(words[x] for x in words.keys())
699 stats['total_known'] = stats['total'] - stats['total_unknown']
700 stats['percentage'] = 100.0*stats['total_known']/stats['total']
701 stats['percentage_unknown'] = 100.0-100.0*stats['total_known']/stats['total']
702 stats['groups'] = 0
703 stats['words'] = len(words)
704 stats['sentences'] = 0 #FIXME
705 stats['wps'] = 0 #FIXME
706 stats['uwps'] = 0 #FIXME
707 stats['language'] = config['language']
709 linked_words = find_linked_words(notes)
710 normalizator = Normalizator(config['language'], linked_words)
712 # filter words by allowed_words_filter
713 if 'allowed_words' in config:
714 allowed_words_filename = config['allowed_words']
715 normalized_allowed_words = [
716 normalizator.normalize(w.rstrip('\n'))
717 for w in readlines_from_file(allowed_words_filename)
718 ]
720 result = {}
721 for w, wn in words.iteritems():
722 if normalizator.normalize(w) in normalized_allowed_words:
723 result[w] = wn
724 words = result
726 words_with_freq = []
727 for k in sorted(words.keys(), key=lambda k: words[k], reverse=True):
728 words_with_freq.append((words[k], k))
730 wgw = find_wordgroups_weights(words_with_freq, normalizator)
731 if not 'no_words_grouping' in config or not config['no_words_grouping']:
732 words_with_freq = sorted(
733 words_with_freq,
734 cmp=lambda x,y:compare_word_pairs(x,y, wgw, normalizator, linked_words),
735 reverse=True)
737 output = print_words_sorted(
738 words_with_freq,
739 stats,
740 normalizator,
741 stats_only=stats_only,
742 compressed_wordlist=compressed_wordlist,
743 compressed_to_line=compressed_to_line,
744 show_range=show_range,
745 show_range_percentage=show_range_percentage,
746 )
749 if ('non_interactive' in config or 'text_stats' in config):
750 codecs.getwriter("utf-8")(sys.stdout).write("".join(output))
751 elif config.get('web', False):
752 web_editor(output)
753 else:
754 (_, temp1) = tempfile.mkstemp(prefix='new-word')
755 (_, temp2) = tempfile.mkstemp(prefix='new-word')
757 with codecs.open(temp1, "w", "utf-8") as f:
758 f.write("".join(output))
759 with codecs.open(temp2, "w", "utf-8") as f:
760 f.write("".join(add_notes(output, notes)))
762 os.putenv('ORIGINAL_TEXT', original_text_tempfile)
763 os.system((
764 "vim"
765 " -c 'setlocal spell spelllang={language}'"
766 " -c 'set keywordprg={language}'"
767 " -c 'set iskeyword=@,48-57,/,.,-,_,+,,,#,$,%,~,=,48-255'"
768 " {filename}"
769 " < /dev/tty > /dev/tty"
770 ).format(language=config['language'], filename=temp2))
772 lines = remove_notes(readlines_from_file(temp2), notes)
774 # compare lines_before and lines_after and return deleted words
775 lines_before = output
776 lines_after = lines
777 deleted_words = []
779 lines_after_set = set(lines_after)
780 for line in lines_before:
781 if line not in lines_after_set:
782 line = line.strip()
783 if ' ' in line:
784 word = re.split('\s+', line, 1)[1]
785 if ' ' in word:
786 word = re.split('\s+', word, 1)[0]
787 deleted_words.append(word)
789 with codecs.open(voc_filename(), "a", "utf-8") as f:
790 f.write("\n".join(deleted_words + ['']))
792 os.unlink(temp1)
793 os.unlink(temp2)
795 os.unlink(original_text_tempfile)
797 (options, args) = parser.parse_args()
798 if options.language:
799 config['language'] = options.language
801 if options.pages:
802 config['pages'] = options.pages
803 else:
804 config['pages'] = ""
806 if options.allowed_words:
807 config['allowed_words'] = options.allowed_words
809 if options.show_range:
810 config['show_range'] = options.show_range
812 if options.show_range_percentage:
813 config['show_range_percentage'] = options.show_range_percentage
815 if options.non_interactive:
816 config['non_interactive'] = True
818 if options.text_stats:
819 config['text_stats'] = True
821 if options.compressed:
822 config['compressed'] = True
824 if options.compressed_to_line:
825 config['compressed_to_line'] = True
827 if options.no_filter:
828 config['no_filter'] = True
830 if options.two_words:
831 config['two_words'] = True
833 if options.three_words:
834 config['three_words'] = True
836 if options.no_words_grouping:
837 config['no_words_grouping'] = True
839 if options.web:
840 config['web'] = True
842 filter_get_words_group_words_add_stat(args)
844 #if options.function:
845 # function_names = {
846 # 'get_words_group_words_add_stat': ,
847 # }
848 # if options.function in function_names:
849 # function_names[options.function](args)
850 # else:
851 # error_message("Unkown function %s.\nAvailable functions:\n%s" % (
852 # options.function, "".join([" "+x for x in sorted(function_names.keys())])))
853 # sys.exit(1)
854 #
858 #os.system("vim")