pogrep/pogrep.py

263 lines
7.9 KiB
Python
Raw Normal View History

2019-12-14 22:06:17 +00:00
"""Find translations examples by grepping in .po files.
2019-11-01 19:30:15 +00:00
"""
2020-04-02 13:43:15 +00:00
__version__ = "0.1.2"
2019-11-01 19:21:22 +00:00
import argparse
2019-11-01 22:17:44 +00:00
import curses
2019-12-14 22:06:17 +00:00
import glob
2019-11-01 19:21:22 +00:00
import os
2019-12-14 22:06:17 +00:00
import sys
2019-11-01 19:21:22 +00:00
from textwrap import fill
from typing import Sequence, NamedTuple, List, Tuple
from shutil import get_terminal_size
2019-11-01 19:21:22 +00:00
import regex
import polib
from tabulate import tabulate
2019-12-14 22:06:17 +00:00
def get_colors():
"""Just returns the CSI codes for red, green, magenta, and reset color.
2019-11-01 22:17:44 +00:00
"""
2019-11-01 19:21:22 +00:00
try:
2019-11-01 22:17:44 +00:00
curses.setupterm()
fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or ""
red = str(curses.tparm(fg_color, 1), "ascii")
2019-12-14 22:06:17 +00:00
green = str(curses.tparm(fg_color, 2), "ascii")
magenta = str(curses.tparm(fg_color, 5), "ascii")
2019-11-01 22:17:44 +00:00
no_color = str(curses.tigetstr("sgr0"), "ascii")
2019-12-14 22:06:17 +00:00
except curses.error:
red, green, magenta = "", "", ""
2019-11-01 22:17:44 +00:00
no_color = ""
2019-12-14 22:06:17 +00:00
return red, green, magenta, no_color
2019-11-01 22:17:44 +00:00
2019-12-14 22:06:17 +00:00
RED, GREEN, MAGENTA, NO_COLOR = get_colors()
2019-11-01 22:17:44 +00:00
def colorize(text, pattern, prefixes=()):
"""Add CSI color codes to make pattern red in text.
Optionally also highlight prefixes (as (line, file) tuples) in
magenta and green.
colorize(" file.py:30:foo bar baz", "bar", [("30:","file.py:")]) gives:
file.py:30:foo bar baz, with the following colors:
| M ||G| |R|
"""
2019-12-14 22:06:17 +00:00
result = regex.sub(pattern, RED + r"\g<0>" + NO_COLOR, text)
for pnum, pfile in prefixes:
prefix = " " + pfile + pnum
prefix_colored = regex.escape(
regex.sub(pattern, RED + r"\g<0>" + NO_COLOR, prefix)
)
2019-12-14 22:06:17 +00:00
if regex.escape(RED) in prefix_colored:
prefix = prefix_colored
prefix_replace = " " + MAGENTA + pfile + GREEN + pnum + NO_COLOR
result = regex.sub(prefix, prefix_replace, result, count=1)
return result
2019-11-01 22:17:44 +00:00
class Match(NamedTuple):
"""Represents a string found in a po file.
"""
file: str
line: int
msgid: str
msgstr: str
def find_in_po(
pattern: str,
path: Sequence[str],
not_in_source: bool = False,
in_translation: bool = False,
) -> Tuple[List[str], List[Match]]:
"""Find the given pattern in the given list of paths or files.
"""
results = []
errors = []
2019-12-14 22:06:17 +00:00
for filename in path:
try:
pofile = polib.pofile(filename)
except OSError:
errors.append("{} doesn't seem to be a .po file".format(filename))
2019-12-14 22:06:17 +00:00
continue
2019-11-01 19:21:22 +00:00
for entry in pofile:
if entry.msgstr and (
(not not_in_source and regex.search(pattern, entry.msgid))
or (in_translation and regex.search(pattern, entry.msgstr))
):
results.append(
Match(filename, entry.linenum, entry.msgid, entry.msgstr)
2019-11-01 19:21:22 +00:00
)
return errors, results
2019-12-14 22:06:17 +00:00
def display_results(
matches: Sequence[Match], pattern: str, line_number: bool, files_with_matches: bool,
):
"""Display matches as a colorfull table.
"""
files = {match.file for match in matches}
if files_with_matches: # Just print filenames
for file in files:
print(MAGENTA + file + NO_COLOR)
return
prefixes = []
table = []
term_width = get_terminal_size()[0]
for match in matches:
left = match.msgid
if line_number:
pnum = str(match.line) + ":"
if len(files) > 1:
pfile = match.file + ":"
else:
pfile = ""
left = pfile + pnum + left
prefixes.append((pnum, pfile))
table.append(
[
fill(left, width=(term_width - 7) // 2),
fill(match.msgstr, width=(term_width - 7) // 2),
]
)
print(colorize(tabulate(table, tablefmt="fancy_grid"), pattern, prefixes))
def process_path(path: Sequence[str], recursive: bool) -> List[str]:
"""Apply the recursive flag to the given paths.
Also check that -r is not used on files, that no directories are
given without -r, and file exists.
"""
2019-12-14 22:06:17 +00:00
files = []
if len(path) == 0:
if not recursive:
2019-12-14 22:06:17 +00:00
sys.exit(0)
return glob.glob("**/*.po", recursive=True)
2019-12-14 22:06:17 +00:00
for elt in path:
if os.path.isfile(elt):
files.append(elt)
elif os.path.isdir(elt):
if recursive:
files.extend(glob.glob(elt + os.sep + "**/*.po", recursive=True))
else:
print(
"{}: {}: Is a directory".format(sys.argv[0], elt), file=sys.stderr
)
2019-12-14 22:06:17 +00:00
sys.exit(1)
else:
print(
"{}: {}: No such file or directory".format(sys.argv[0], elt),
file=sys.stderr,
)
2019-12-14 22:06:17 +00:00
sys.exit(1)
return files
2019-11-01 19:21:22 +00:00
def parse_args() -> argparse.Namespace:
"""Parse command line arguments.
"""
2019-11-01 19:21:22 +00:00
parser = argparse.ArgumentParser(description="Find translated words.")
parser.add_argument(
"-F",
"--fixed-strings",
action="store_true",
help="Interpret pattern as fixed string, not regular expressions.",
)
parser.add_argument(
"-i",
"--ignore-case",
action="store_true",
help="Ignore case distinctions, so that characters that differ "
"only in case match each other.",
)
parser.add_argument(
"-w",
"--word-regexp",
action="store_true",
help="Select only those lines containing matches that form whole words.",
)
parser.add_argument(
"-n",
"--line-number",
action="store_true",
help="Prefix each line of output with the 1-based line number within "
"its input file.",
)
parser.add_argument(
"-l",
"--files-with-matches",
action="store_true",
help="Suppress normal output; instead print the name of each input file "
"from which output would normally have been printed. "
"The scanning will stop on the first match.",
)
parser.add_argument(
"-s",
"--no-messages",
action="store_true",
help="Suppress error messages about nonexistent or unreadable files.",
)
parser.add_argument(
"-r",
"--recursive",
action="store_true",
help="Read all files under each directory, recursively, following "
"symbolic links only if they are on the command line. "
"Note that if no file operand is given, pogrep searches "
"the working directory.",
)
parser.add_argument(
"--translation",
action="store_true",
help="search pattern in translated text (result printed on the right column",
)
parser.add_argument(
"--no-source",
action="store_true",
help="do NOT search pattern in original text (result printed on the "
"left column",
)
parser.add_argument(
"--exclude-dir",
help="Skip any command-line directory with a name suffix that matches "
"the pattern. "
"When searching recursively, skip any subdirectory whose base name "
"matches GLOB. "
"Ignore any redundant trailing slashes in GLOB.",
)
2019-11-01 19:21:22 +00:00
parser.add_argument("pattern")
parser.add_argument("path", nargs="*")
2019-11-01 19:21:22 +00:00
return parser.parse_args()
def main():
"""Command line entry point.
"""
2019-11-01 19:21:22 +00:00
args = parse_args()
2019-12-14 22:06:17 +00:00
if args.fixed_strings:
args.pattern = regex.escape(args.pattern)
if args.word_regexp:
args.pattern = r"\b" + args.pattern + r"\b"
if args.ignore_case:
args.pattern = r"(?i)" + args.pattern
files = process_path(args.path, args.recursive)
if args.exclude_dir:
files = [f for f in files if args.exclude_dir.rstrip(os.sep) + os.sep not in f]
errors, results = find_in_po(args.pattern, files, args.no_source, args.translation)
if not args.no_messages:
for error in errors:
print(error, file=sys.stderr)
display_results(
results, args.pattern, args.line_number, args.files_with_matches,
)
2019-11-01 19:21:22 +00:00
if __name__ == "__main__":
main()