Compare commits

...

7 Commits

6 changed files with 101 additions and 37 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
venv/
*.egg-info/
__pycache__/
*.test_*.srt
dist/

View File

@ -1,9 +1,25 @@
# subprompt
Substitute every match of a regex in multiple files - with confirmation prompts. Usage:
```
subprompt [REGEX] [SUB] [FILES...]
```
Install it through PyPi:
Interactively change every line matching a regex in multiple files.
## Installation
Through PyPI:
```
python3.9 -m pip install subprompt
```
## Usage
```
usage: subprompt [-h] (-d | -r SUB) [-n N] REGEX FILES [FILES ...]
Modifies lines matched by a regex interactively
positional arguments:
REGEX
FILES
optional arguments:
-h, --help show this help message and exit
-d delete line
-r SUB replace match with expression
-n N size of lines preview (default=3)
```

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
- Refactor code, break into functions and clean everything
- Add unit tests
- Improve efficiency
- (maybe?) Add append and prepend flags (already possible with sub expr, so maybe not needed)

View File

@ -1 +1,3 @@
colorama==0.4.3
build==0.9.0
twine==4.0.1

View File

@ -1,6 +1,6 @@
[metadata]
name = subprompt
version = 0.0.4
version = 0.0.9
author = Augusto Lenz Gunsch
author_email = augusto@augustogunsch.com
description = Substitute Regex in files with prompt confirmation

View File

@ -25,54 +25,83 @@
import re
import hashlib
import os
import argparse
from sys import argv, stderr
from colorama import init, Fore, Back, Style
init(autoreset=True)
class Filter:
def __init__(self, regex, sub):
def __init__(self, args):
self.replace_all = False
self.quit_loop = False
self.sub_count = 0
self.match_count = 0
self.regex = re.compile(regex)
self.sub = sub
self.regex = re.compile(args.regex)
self.sub = args.r
self.delete = args.d
self.spread = args.n+1
def _prompt(self, matchobj, line, line_n, fname):
self.lines_to_delete = []
def _number_lines(_, lines, start_n):
return [Fore.YELLOW + Style.BRIGHT +
str(n + start_n + 1) +
Fore.RESET + Style.RESET_ALL +
':' + line
for (n, line) in enumerate(lines)]
def _prompt(self, matchobj, lines, line, line_n, fname):
curr_match = matchobj.group(0)
if self.quit_loop:
return curr_match
replaced_match = self.regex.sub(self.sub, curr_match)
if not self.delete:
replaced_match = self.regex.sub(self.sub, curr_match)
if replaced_match == curr_match:
return replaced_match
if replaced_match == curr_match:
return replaced_match
self.match_count += 1
if self.replace_all:
self.sub_count += 1
if self.delete:
self.lines_to_delete.append(line_n)
return None
return replaced_match
start_n = max(0, line_n - self.spread)
end_n = min(len(lines)+1, line_n + self.spread)
rel_line_n = line_n-start_n
cut = lines[start_n:end_n]
highlighted = line.replace(curr_match,
Back.RED + curr_match + Back.RESET)
replaced = line.replace(curr_match,
Back.YELLOW + Fore.BLACK + replaced_match + Back.RESET + Fore.RESET)
cut_highlighted = cut
cut_highlighted[rel_line_n] = highlighted
cut_highlighted = self._number_lines(cut_highlighted, start_n)
cut_replaced = cut
if not self.delete:
cut_replaced[rel_line_n] = line.replace(curr_match,
Back.YELLOW + Fore.BLACK + replaced_match + Back.RESET + Fore.RESET)
else:
cut_replaced.pop(rel_line_n)
cut_replaced = self._number_lines(cut_replaced, start_n)
print(Fore.GREEN + Style.BRIGHT + fname)
print(Fore.YELLOW + Style.BRIGHT + str(line_n), end='')
print(':{0}'.format(highlighted))
print(''.join(cut_highlighted))
print('Becomes the following:')
print(Fore.YELLOW + Style.BRIGHT + str(line_n), end='')
print(':{0}'.format(replaced))
print()
print(''.join(cut_replaced))
while True:
answer = input('Confirm the change? [Y/n/a/q] ').lower()
@ -80,6 +109,9 @@ class Filter:
if answer in ['y', 'yes', '']:
self.sub_count += 1
print()
if self.delete:
self.lines_to_delete.append(line_n)
return None
return replaced_match
elif answer in ['n', 'no']:
@ -89,6 +121,9 @@ class Filter:
elif answer in ['a', 'all']:
self.sub_count += 1
self.replace_all = True
if self.delete:
self.lines_to_delete.append(line_n)
return None
return replaced_match
elif answer in ['q', 'quit']:
@ -101,17 +136,23 @@ class Filter:
with open(fname, 'r') as file:
contents = file.read()
lines = contents.splitlines()
lines = contents.splitlines(keepends=True)
lines = [
self.regex.sub(
lambda matchobj: self._prompt(matchobj, line, line_n, fname),
lambda matchobj: self._prompt(matchobj, lines, line, line_n, fname),
line
)
for (line_n, line) in enumerate(lines)
]
new_contents = '\n'.join(lines)
if self.delete:
for line in reversed(self.lines_to_delete):
lines.pop(line)
self.lines_to_delete = []
new_contents = ''.join(lines)
hash_old = hashlib.md5(contents.encode())
hash_new = hashlib.md5(new_contents.encode())
@ -119,26 +160,29 @@ class Filter:
if hash_old.digest() != hash_new.digest():
with open(fname, 'w') as file:
file.write(new_contents)
file.write('\n')
def run(args):
if len(args) < 3:
print('usage: subprompt [REGEX] [SUB] [FILES...]', file=stderr)
exit(1)
parser = argparse.ArgumentParser(description='Modifies lines matched by a regex interactively')
parser.add_argument('regex', metavar='REGEX', type=str)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument('-d', help='delete line', action='store_true')
action.add_argument('-r', help='replace match with expression', type=str, metavar='SUB')
parser.add_argument('files', metavar='FILES', nargs='+', type=str)
parser.add_argument('-n', help='size of lines preview (default=3)', type=int, default=3)
regex = args[0]
sub = args[1]
files = args[2:]
args = parser.parse_args(args)
filter_obj = Filter(regex, sub)
filter_obj = Filter(args)
for file in files:
for file in args.files:
# Check if file is writable
if os.access(file, os.W_OK):
filter_obj.filter_file(file)
if filter_obj.quit_loop:
break
else:
print('WARNING: File {0} is not writable. Skipping...'.format(file))
if filter_obj.match_count == 0:
print(Fore.RED + Style.BRIGHT + 'No matches found.')
@ -148,8 +192,7 @@ def run(args):
Fore.WHITE + str(filter_obj.match_count) + Fore.YELLOW))
def main():
argv.pop(0)
run(argv)
run(argv[1:])
if __name__ == '__main__':
main()