Skip to content

Instantly share code, notes, and snippets.

@erenon
Created July 15, 2022 08:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erenon/0c9c8965a175ab5d786c82a0d3101213 to your computer and use it in GitHub Desktop.
Save erenon/0c9c8965a175ab5d786c82a0d3101213 to your computer and use it in GitHub Desktop.
Rewrite matches
#!/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