Repository: asottile/babi

Issue:
regex replace invalid replace causes crash
```
$ nano babi/file.py 
Traceback (most recent call last):
  File "/home/asottile/bin/nano", line 8, in <module>
    sys.exit(main())
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/main.py", line 108, in main
    return c_main(stdscr, args, stdin)
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/main.py", line 50, in c_main
    res = _edit(screen, stdin)
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/main.py", line 30, in _edit
    ret = Screen.DISPATCH[key.keyname](screen)
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/screen.py", line 383, in replace
    self.file.replace(self, search_response, response)
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/file.py", line 142, in clear_selection_inner
    ret = func(self, *args, **kwargs)
  File "/home/asottile/opt/venv/lib/python3.6/site-packages/babi/file.py", line 465, in replace
    replaced = match.expand(replace)
  File "/home/asottile/opt/venv/lib/python3.6/re.py", line 321, in _expand
    template = sre_parse.parse_template(template, pattern)
  File "/home/asottile/opt/venv/lib/python3.6/sre_parse.py", line 895, in parse_template
    this = sget()
  File "/home/asottile/opt/venv/lib/python3.6/sre_parse.py", line 255, in get
    self.__next()
  File "/home/asottile/opt/venv/lib/python3.6/sre_parse.py", line 245, in __next
    self.string, len(self.string) - 1) from None
sre_constants.error: bad escape (end of pattern) at position 24
```

for example, try a replace with a trailing backslash

Discussion:
asottile: I think ideally the strings would be validated up front instead of at the replacement time if possible -- that way a more direct error message can be given "invalid replacement string" or "invalid regex" or some such instead of just "replacement failed" without an explanation
AndrewLaneX: Is there a way to validate it? If not, the regex is already validated so maybe "invalid replacement" would just work?
asottile: > Is there a way to validate it? If not, the regex is already validated so maybe "invalid replacement" would just work?

I'm not sure -- there probably is.  the ideal user experience is to let them know before they start searching around and replacing though -- so ideally it would catch it immediately after putting in the replacement string 😢 

Patch:
diff --git a/babi/screen.py b/babi/screen.py
index 9d725d4..c86bc71 100644
--- a/babi/screen.py
+++ b/babi/screen.py
@@ -7,6 +7,7 @@ import hashlib
 import os
 import re
 import signal
+import sre_parse
 import sys
 from typing import Generator
 from typing import NamedTuple
@@ -419,7 +420,12 @@ class Screen:
                 'replace with', history='replace', allow_empty=True,
             )
             if response is not PromptResult.CANCELLED:
-                self.file.replace(self, search_response, response)
+                try:
+                    sre_parse.parse_template(response, search_response)
+                except re.error:
+                    self.status.update('invalid replacement string')
+                else:
+                    self.file.replace(self, search_response, response)
 
     def command(self) -> EditResult | None:
         response = self.prompt('', history='command')


Test patch:
diff --git a/tests/features/replace_test.py b/tests/features/replace_test.py
index f42cf10..83e489c 100644
--- a/tests/features/replace_test.py
+++ b/tests/features/replace_test.py
@@ -22,6 +22,16 @@ def test_replace_invalid_regex(run):
         h.await_text("invalid regex: '('")
 
 
+def test_replace_invalid_replacement(run, ten_lines):
+    with run(str(ten_lines)) as h, and_exit(h):
+        h.press('^\\')
+        h.await_text('search (to replace):')
+        h.press_and_enter('line_0')
+        h.await_text('replace with:')
+        h.press_and_enter('\\')
+        h.await_text('invalid replacement string')
+
+
 def test_replace_cancel_at_replace_string(run):
     with run() as h, and_exit(h):
         h.press('^\\')