Created
July 15, 2022 08:40
-
-
Save erenon/0c9c8965a175ab5d786c82a0d3101213 to your computer and use it in GitHub Desktop.
Rewrite matches
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/python | |
# Replace matching patterns at the specified locations | |
# | |
# On stdin it expects locations: | |
# | |
# <file_path>|<line> col <col>| match | |
# | |
# For example: | |
# | |
# path/to/file.rs|13 col 9| foo | |
# path/to/other_file.rs|62 col 13| foobar | |
# path/dir/file.txt|51 col 13| barfoo | |
# | |
# This is the format of the vim location list, that can be populated e.g: by a LSP server. | |
# | |
# This program opens each file specified on the input and goes over it line-by-line. | |
# Lines outside of the match location specified on the input are kept verbatim. | |
# The match location begins at <line>:<col>, as specified by the input, | |
# and ends where the opening and closing markers balance out each other, | |
# possibly in a subsequent lines: | |
mark_begin = '(' | |
mark_end = ')' | |
# Markers must be a single character. | |
# This way function calls spanning multiple lines can be found. | |
# The search for the markers is pretty crude and input agnostic, | |
# it does not treat espaced markers or markers inside string literals specially. | |
# To avoid overruns, the maximum line count can be limited: | |
# after this many lines the location automatically ends, regardless the marker balance. | |
max_location_lines = 50 | |
# Each line inside the a matching location is tested against the given regular expressions, | |
# and gets replaced by them as specified. | |
# Each entry in replacements is a tuple of (pattern, replacement). | |
# replacement can be a function, see python re::sub. | |
replacements = [ | |
('bad', 'good'), | |
('spoiled (.+)', 'fresh \\1'), | |
] | |
# Only the first matching expression gets applied. | |
# The matching lines with replacement applied get written back to the input file. | |
# Non-matching lines are kept verbatim, with an optional warning: | |
warn_non_matching_line = True | |
# Files with multiple locations are supported by opening/writing the same file multiple times. | |
# This works only as long as replacements do not change the number of lines. | |
# Overlapping matches might work but not supported. | |
import io | |
import re | |
import shutil | |
import sys | |
def update_balance(s, balance): | |
for c in s: | |
if c == mark_begin: | |
balance +=1 | |
elif c == mark_end: | |
balance -= 1 | |
if balance == 0: | |
return 0 | |
return balance | |
def apply_replacements(s): | |
for pattern,repl in replacements: | |
new,matched = re.subn(pattern, repl, s) | |
if matched: | |
return new | |
if warn_non_matching_line: | |
print(f'NOMATCH {s.rstrip()}') | |
return s | |
def rewrite_file(f, begin_line, begin_col): | |
curline = 0 | |
balance = 0 | |
location_lines = 0 | |
output = io.StringIO() | |
for line in f: | |
curline += 1 | |
if curline < begin_line: | |
output.write(line) | |
elif curline >= begin_line: | |
line_loc_part = line if curline > begin_line else line[begin_col:] | |
balance = update_balance(line_loc_part, balance) | |
sline = apply_replacements(line) | |
output.write(sline) | |
location_lines += 1 | |
if balance == 0 or location_lines > max_location_lines: | |
break | |
output.write(f.read()) | |
return output | |
def main(): | |
for flc in sys.stdin: | |
path,linecol = flc.split('|', 2)[:2] | |
begin_line,_,begin_col = linecol.partition(' col ') | |
begin_line = int(begin_line) | |
begin_col = int(begin_col) | |
with open(path) as f: | |
output = rewrite_file(f, begin_line, begin_col) | |
with open(path, 'w') as f: | |
output.seek(0) | |
shutil.copyfileobj(output, f) | |
output.close() | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment