Improve argument parsing and add delete line option

This commit is contained in:
Augusto Gunsch 2022-11-15 17:50:56 +01:00
parent c5323ad2f1
commit de278ae0ba
No known key found for this signature in database
GPG Key ID: F7EEFE29825C72DC
2 changed files with 47 additions and 25 deletions

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = subprompt name = subprompt
version = 0.0.4 version = 0.0.5
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

View File

@ -25,23 +25,25 @@
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)
# Config
line_spread = 3
class Filter: class Filter:
def __init__(self, regex, sub): def __init__(self, args):
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(regex) self.regex = re.compile(args.regex)
self.sub = sub self.sub = args.r
self.delete = args.d
self.spread = args.n
self.lines_to_delete = []
def _number_lines(_, lines, start_n): def _number_lines(_, lines, start_n):
return [Fore.YELLOW + Style.BRIGHT + return [Fore.YELLOW + Style.BRIGHT +
@ -56,32 +58,40 @@ class Filter:
if self.quit_loop: if self.quit_loop:
return curr_match 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: 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 - line_spread) start_n = max(0, line_n - self.spread)
end_n = min(len(lines), line_n + line_spread) end_n = min(len(lines), line_n + self.spread)
cut = lines[start_n:end_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,
Back.YELLOW + Fore.BLACK + replaced_match + Back.RESET + Fore.RESET)
cut_highlighted = cut cut_highlighted = cut
cut_highlighted[line_n] = highlighted cut_highlighted[line_n] = highlighted
cut_highlighted = self._number_lines(cut_highlighted, start_n) cut_highlighted = self._number_lines(cut_highlighted, start_n)
cut_replaced = cut cut_replaced = cut
cut_replaced[line_n] = replaced
if not self.delete:
cut_replaced[line_n] = line.replace(curr_match,
Back.YELLOW + Fore.BLACK + replaced_match + Back.RESET + Fore.RESET)
else:
cut_replaced.pop(line_n)
cut_replaced = self._number_lines(cut_replaced, start_n) cut_replaced = self._number_lines(cut_replaced, start_n)
print(Fore.GREEN + Style.BRIGHT + fname) print(Fore.GREEN + Style.BRIGHT + fname)
@ -98,6 +108,9 @@ 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']:
@ -107,6 +120,9 @@ 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']:
@ -129,6 +145,9 @@ class Filter:
for (line_n, line) in enumerate(lines) for (line_n, line) in enumerate(lines)
] ]
for line in reversed(self.lines_to_delete):
lines.pop(line)
new_contents = '\n'.join(lines) new_contents = '\n'.join(lines)
hash_old = hashlib.md5(contents.encode()) hash_old = hashlib.md5(contents.encode())
@ -140,23 +159,27 @@ class Filter:
file.write('\n') file.write('\n')
def run(args): def run(args):
if len(args) < 3: parser = argparse.ArgumentParser(description='Modifies lines matched by a regex interactively')
print('usage: subprompt [REGEX] [SUB] [FILES...]', file=stderr) parser.add_argument('regex', metavar='REGEX', type=str)
exit(1) 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)
parser.add_argument('files', metavar='FILES', nargs='+', type=str)
parser.add_argument('-n', help='size lines preview (default=3)', type=int, default=3)
regex = args[0] args = parser.parse_args(args)
sub = args[1]
files = args[2:]
filter_obj = Filter(regex, sub) filter_obj = Filter(args)
for file in files: for file in args.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.')
@ -166,8 +189,7 @@ 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():
argv.pop(0) run(argv[1:])
run(argv)
if __name__ == '__main__': if __name__ == '__main__':
main() main()