Compare commits
No commits in common. "master" and "0.0.4" have entirely different histories.
|
@ -1,4 +1,5 @@
|
||||||
venv/
|
venv/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
*.test_*.srt
|
||||||
dist/
|
dist/
|
||||||
|
|
26
README.md
26
README.md
|
@ -1,25 +1,9 @@
|
||||||
# subprompt
|
# subprompt
|
||||||
Interactively change every line matching a regex in multiple files.
|
Substitute every match of a regex in multiple files - with confirmation prompts. Usage:
|
||||||
|
```
|
||||||
## Installation
|
subprompt [REGEX] [SUB] [FILES...]
|
||||||
Through PyPI:
|
```
|
||||||
|
Install it through PyPi:
|
||||||
```
|
```
|
||||||
python3.9 -m pip install subprompt
|
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
4
TODO.md
|
@ -1,4 +0,0 @@
|
||||||
- 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)
|
|
|
@ -1,3 +1 @@
|
||||||
colorama==0.4.3
|
colorama==0.4.3
|
||||||
build==0.9.0
|
|
||||||
twine==4.0.1
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = subprompt
|
name = subprompt
|
||||||
version = 0.0.9
|
version = 0.0.4
|
||||||
author = Augusto Lenz Gunsch
|
author = Augusto Lenz Gunsch
|
||||||
author_email = augusto@augustogunsch.com
|
author_email = augusto@augustogunsch.com
|
||||||
description = Substitute Regex in files with prompt confirmation
|
description = Substitute Regex in files with prompt confirmation
|
||||||
|
|
|
@ -25,83 +25,54 @@
|
||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import argparse
|
|
||||||
from sys import argv, stderr
|
from sys import argv, stderr
|
||||||
from colorama import init, Fore, Back, Style
|
from colorama import init, Fore, Back, Style
|
||||||
|
|
||||||
init(autoreset=True)
|
init(autoreset=True)
|
||||||
|
|
||||||
class Filter:
|
class Filter:
|
||||||
def __init__(self, args):
|
def __init__(self, regex, sub):
|
||||||
self.replace_all = False
|
self.replace_all = False
|
||||||
self.quit_loop = False
|
self.quit_loop = False
|
||||||
self.sub_count = 0
|
self.sub_count = 0
|
||||||
self.match_count = 0
|
self.match_count = 0
|
||||||
|
|
||||||
self.regex = re.compile(args.regex)
|
self.regex = re.compile(regex)
|
||||||
self.sub = args.r
|
self.sub = sub
|
||||||
self.delete = args.d
|
|
||||||
self.spread = args.n+1
|
|
||||||
|
|
||||||
self.lines_to_delete = []
|
def _prompt(self, matchobj, line, line_n, fname):
|
||||||
|
|
||||||
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)
|
curr_match = matchobj.group(0)
|
||||||
|
|
||||||
if self.quit_loop:
|
if self.quit_loop:
|
||||||
return curr_match
|
return curr_match
|
||||||
|
|
||||||
if not self.delete:
|
replaced_match = self.regex.sub(self.sub, curr_match)
|
||||||
replaced_match = self.regex.sub(self.sub, curr_match)
|
|
||||||
|
|
||||||
if replaced_match == curr_match:
|
if replaced_match == curr_match:
|
||||||
return replaced_match
|
return replaced_match
|
||||||
|
|
||||||
self.match_count += 1
|
self.match_count += 1
|
||||||
|
|
||||||
if self.replace_all:
|
if self.replace_all:
|
||||||
self.sub_count += 1
|
self.sub_count += 1
|
||||||
if self.delete:
|
|
||||||
self.lines_to_delete.append(line_n)
|
|
||||||
return None
|
|
||||||
return replaced_match
|
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,
|
highlighted = line.replace(curr_match,
|
||||||
Back.RED + curr_match + Back.RESET)
|
Back.RED + curr_match + Back.RESET)
|
||||||
|
replaced = line.replace(curr_match,
|
||||||
cut_highlighted = cut
|
Back.YELLOW + Fore.BLACK + replaced_match + Back.RESET + Fore.RESET)
|
||||||
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.GREEN + Style.BRIGHT + fname)
|
||||||
|
|
||||||
print(''.join(cut_highlighted))
|
print(Fore.YELLOW + Style.BRIGHT + str(line_n), end='')
|
||||||
print('Becomes the following:')
|
print(':{0}'.format(highlighted))
|
||||||
print()
|
|
||||||
print(''.join(cut_replaced))
|
|
||||||
|
|
||||||
|
print('Becomes the following:')
|
||||||
|
|
||||||
|
print(Fore.YELLOW + Style.BRIGHT + str(line_n), end='')
|
||||||
|
print(':{0}'.format(replaced))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
answer = input('Confirm the change? [Y/n/a/q] ').lower()
|
answer = input('Confirm the change? [Y/n/a/q] ').lower()
|
||||||
|
@ -109,9 +80,6 @@ class Filter:
|
||||||
if answer in ['y', 'yes', '']:
|
if answer in ['y', 'yes', '']:
|
||||||
self.sub_count += 1
|
self.sub_count += 1
|
||||||
print()
|
print()
|
||||||
if self.delete:
|
|
||||||
self.lines_to_delete.append(line_n)
|
|
||||||
return None
|
|
||||||
return replaced_match
|
return replaced_match
|
||||||
|
|
||||||
elif answer in ['n', 'no']:
|
elif answer in ['n', 'no']:
|
||||||
|
@ -121,9 +89,6 @@ class Filter:
|
||||||
elif answer in ['a', 'all']:
|
elif answer in ['a', 'all']:
|
||||||
self.sub_count += 1
|
self.sub_count += 1
|
||||||
self.replace_all = True
|
self.replace_all = True
|
||||||
if self.delete:
|
|
||||||
self.lines_to_delete.append(line_n)
|
|
||||||
return None
|
|
||||||
return replaced_match
|
return replaced_match
|
||||||
|
|
||||||
elif answer in ['q', 'quit']:
|
elif answer in ['q', 'quit']:
|
||||||
|
@ -136,23 +101,17 @@ class Filter:
|
||||||
with open(fname, 'r') as file:
|
with open(fname, 'r') as file:
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
|
|
||||||
lines = contents.splitlines(keepends=True)
|
lines = contents.splitlines()
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
self.regex.sub(
|
self.regex.sub(
|
||||||
lambda matchobj: self._prompt(matchobj, lines, line, line_n, fname),
|
lambda matchobj: self._prompt(matchobj, line, line_n, fname),
|
||||||
line
|
line
|
||||||
)
|
)
|
||||||
for (line_n, line) in enumerate(lines)
|
for (line_n, line) in enumerate(lines)
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.delete:
|
new_contents = '\n'.join(lines)
|
||||||
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_old = hashlib.md5(contents.encode())
|
||||||
hash_new = hashlib.md5(new_contents.encode())
|
hash_new = hashlib.md5(new_contents.encode())
|
||||||
|
@ -160,29 +119,26 @@ class Filter:
|
||||||
if hash_old.digest() != hash_new.digest():
|
if hash_old.digest() != hash_new.digest():
|
||||||
with open(fname, 'w') as file:
|
with open(fname, 'w') as file:
|
||||||
file.write(new_contents)
|
file.write(new_contents)
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
def run(args):
|
def run(args):
|
||||||
parser = argparse.ArgumentParser(description='Modifies lines matched by a regex interactively')
|
if len(args) < 3:
|
||||||
parser.add_argument('regex', metavar='REGEX', type=str)
|
print('usage: subprompt [REGEX] [SUB] [FILES...]', file=stderr)
|
||||||
action = parser.add_mutually_exclusive_group(required=True)
|
exit(1)
|
||||||
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)
|
|
||||||
|
|
||||||
args = parser.parse_args(args)
|
regex = args[0]
|
||||||
|
sub = args[1]
|
||||||
|
files = args[2:]
|
||||||
|
|
||||||
filter_obj = Filter(args)
|
filter_obj = Filter(regex, sub)
|
||||||
|
|
||||||
for file in args.files:
|
for file in files:
|
||||||
# Check if file is writable
|
# Check if file is writable
|
||||||
if os.access(file, os.W_OK):
|
if os.access(file, os.W_OK):
|
||||||
filter_obj.filter_file(file)
|
filter_obj.filter_file(file)
|
||||||
|
|
||||||
if filter_obj.quit_loop:
|
if filter_obj.quit_loop:
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
print('WARNING: File {0} is not writable. Skipping...'.format(file))
|
|
||||||
|
|
||||||
if filter_obj.match_count == 0:
|
if filter_obj.match_count == 0:
|
||||||
print(Fore.RED + Style.BRIGHT + 'No matches found.')
|
print(Fore.RED + Style.BRIGHT + 'No matches found.')
|
||||||
|
@ -192,7 +148,8 @@ def run(args):
|
||||||
Fore.WHITE + str(filter_obj.match_count) + Fore.YELLOW))
|
Fore.WHITE + str(filter_obj.match_count) + Fore.YELLOW))
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
run(argv[1:])
|
argv.pop(0)
|
||||||
|
run(argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Reference in New Issue