Repository: nvaccess/nvda

Issue:
Unable to use backslash in the replacement value of dictionaries
### Context

Issue found while while reviewing the list of modified files in SVN update status window.

### Steps to reproduce:

* Open a new entry in the temporary dictionary
* Ine pattern field put "`p:\a\very\long\path\to\my\project\root\folder\`"
* In replace field, put: "`root\`"
* Leave other fields as they are, specifically leave the radio button type on "Anywhere"
* Press OK

### Actual behavior:
I get the following error message box:
> Regular Expression error: "bad escape (end of pattern) at position 4".

### Expected behavior:
* When regular expression is not selected as entry type, I should be able to use backslash character normally in the dictionary replace field.
* I should not get any message related to regexps if I have not selected "Regular expression" as dictionary entry type.

### NVDA logs, crash dumps and other attachments:
Following debugWarning found in the log:
```
DEBUGWARNING - gui.speechDict.DictionaryEntryDialog.onOk (17:27:09.948) - MainThread (19688):
Could not add dictionary entry due to (regex error) : bad escape (end of pattern) at position 4
```



### System configuration
#### NVDA installed/portable/running from source:
Installed
#### NVDA version:
2022.4
#### Windows version:
Windows 10 20H2 (AMD64) build 19042.2364
#### Name and version of other software in use when reproducing the issue:
N/A
#### Other information about your system:
N/A
### Other questions
#### Does the issue still occur after restarting your computer?
Yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
Not tested but surely not new.
#### If NVDA add-ons are disabled, is your problem still occurring?
Yes
#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Not tested;unrelated.



Discussion:

Resolving patch:
diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py
--- a/source/gui/speechDict.py
+++ b/source/gui/speechDict.py
@@ -1,10 +1,12 @@
 # -*- coding: UTF-8 -*-
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2007-2022 NV Access Limited, Peter Vágner, Mesar Hameed, Joseph Lee,
+# Copyright (C) 2007-2023 NV Access Limited, Peter Vágner, Mesar Hameed, Joseph Lee,
 # Aaron Cannon, Ethan Holliger, Julien Cochuyt, Thomas Stivers, Cyrille Bougot, Aleksey Sadovoy
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
+from re import error as RegexpError
+
 from abc import abstractmethod
 import wx
 
@@ -94,25 +96,44 @@ def onOk(self, evt):
 			)
 			self.patternTextCtrl.SetFocus()
 			return
+		entryType = self.getType()
 		try:
 			dictEntry = self.dictEntry = speechDictHandler.SpeechDictEntry(
 				self.patternTextCtrl.GetValue(),
 				self.replacementTextCtrl.GetValue(),
 				self.commentTextCtrl.GetValue(),
 				bool(self.caseSensitiveCheckBox.GetValue()),
-				self.getType()
+				entryType,
 			)
+		except RegexpError as e:
+			log.debugWarning(f"Could not add dictionary entry due to regex error in the pattern field : {e}")
+			if entryType != speechDictHandler.ENTRY_TYPE_REGEXP:
+				raise e
+			gui.messageBox(
+				# Translators: This is an error message to let the user know that the dictionary entry is not valid.
+				_("Regular Expression error in the pattern field: \"{error}\".").format(error=e),
+				# Translators: The title of an error message raised by the Dictionary Entry dialog
+				_("Dictionary Entry Error"),
+				wx.OK | wx.ICON_WARNING,
+				self
+			)
+			self.patternTextCtrl.SetFocus()
+			return
+		try:
 			dictEntry.sub("test")  # Ensure there are no grouping error (#11407)
-		except Exception as e:
-			log.debugWarning("Could not add dictionary entry due to (regex error) : %s" % e)
+		except RegexpError as e:
+			log.debugWarning(f"Could not add dictionary entry due to regex error in the replacement field : {e}")
+			if entryType != speechDictHandler.ENTRY_TYPE_REGEXP:
+				raise e
 			gui.messageBox(
 				# Translators: This is an error message to let the user know that the dictionary entry is not valid.
-				_("Regular Expression error: \"%s\".") % e,
+				_("Regular Expression error in the replacement field: \"{error}\".").format(error=e),
 				# Translators: The title of an error message raised by the Dictionary Entry dialog
 				_("Dictionary Entry Error"),
 				wx.OK | wx.ICON_WARNING,
 				self
 			)
+			self.replacementTextCtrl.SetFocus()
 			return
 		evt.Skip()
 
diff --git a/source/speechDictHandler/__init__.py b/source/speechDictHandler/__init__.py
--- a/source/speechDictHandler/__init__.py
+++ b/source/speechDictHandler/__init__.py
@@ -1,8 +1,7 @@
-#speechDictHandler.py
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2006-2017 NVDA Contributors <http://www.nvda-project.org/>
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2006-2023 NVDA Contributors <http://www.nvda-project.org/>
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
 import re
 import globalVars
@@ -41,8 +40,11 @@ def __init__(self, pattern, replacement,comment,caseSensitive=True,type=ENTRY_TY
 		self.caseSensitive=caseSensitive
 		self.type=type
 
-	def sub(self, text):
-		replacement=self.replacement
+	def sub(self, text: str) -> str:
+		if self.type == ENTRY_TYPE_REGEXP:
+			replacement = self.replacement
+		else:
+			replacement = re.escape(self.replacement)
 		return self.compiled.sub(replacement, text)
 
 class SpeechDict(list):


Test patch: