replace_regex
Replace content in files using Python-style regular expressions. Supports multi-line matching and backreferences. Ideal for large code sections or precise modifications without specifying exact content. Use wildcards for efficiency.
Instructions
Replaces one or more occurrences of the given regular expression. This is the preferred way to replace content in a file whenever the symbol-level tools are not appropriate. Even large sections of code can be replaced by providing a concise regular expression of the form "beginning.*?end-of-text-to-be-replaced". Always try to use wildcards to avoid specifying the exact content of the code to be replaced, especially if it spans several lines.
IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| allow_multiple_occurrences | No | If True, the regex may match multiple occurrences in the file and all of them will be replaced. If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). | |
| regex | Yes | A Python-style regular expression, matches of which will be replaced. Dot matches all characters, multi-line matching is enabled. | |
| relative_path | Yes | The relative path to the file. | |
| repl | Yes | The string to replace the matched content with, which may contain backreferences like \1, \2, etc. |
Implementation Reference
- src/serena/tools/file_tools.py:160-247 (handler)The ReplaceContentTool class provides the core implementation for replacing content in files using literal strings or regex patterns. The 'apply' method is the public tool interface, delegating to 'replace_content' for the actual regex replacement logic using Python's re module with DOTALL and MULTILINE flags. This is the handler executed for the 'replace_content' tool, which 'replace_regex' maps to.class ReplaceContentTool(Tool, ToolMarkerCanEdit): """ Replaces content in a file (optionally using regular expressions). """ def apply( self, relative_path: str, needle: str, repl: str, mode: Literal["literal", "regex"], allow_multiple_occurrences: bool = False, ) -> str: r""" Replaces one or more occurrences of a given pattern in a file with new content. This is the preferred way to replace content in a file whenever the symbol-level tools are not appropriate. VERY IMPORTANT: The "regex" mode allows very large sections of code to be replaced without fully quoting them! Use a regex of the form "beginning.*?end-of-text-to-be-replaced" to be faster and more economical! ALWAYS try to use wildcards to avoid specifying the exact content to be replaced, especially if it spans several lines. Note that you cannot make mistakes, because if the regex should match multiple occurrences while you disabled `allow_multiple_occurrences`, an error will be returned, and you can retry with a revised regex. Therefore, using regex mode with suitable wildcards is usually the best choice! :param relative_path: the relative path to the file :param needle: the string or regex pattern to search for. If `mode` is "literal", this string will be matched exactly. If `mode` is "regex", this string will be treated as a regular expression (syntax of Python's `re` module, with flags DOTALL and MULTILINE enabled). :param repl: the replacement string (verbatim). If mode is "regex", the string can contain backreferences to matched groups in the needle regex, specified using the syntax $!1, $!2, etc. for groups 1, 2, etc. :param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted. :param allow_multiple_occurrences: if True, the regex may match multiple occurrences in the file and all of them will be replaced. If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ return self.replace_content( relative_path, needle, repl, mode=mode, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True ) def replace_content( self, relative_path: str, needle: str, repl: str, mode: Literal["literal", "regex"], allow_multiple_occurrences: bool = False, require_not_ignored: bool = True, ) -> str: """ Performs the replacement, with additional options not exposed in the tool. This function can be used internally by other tools. """ self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored) with EditedFileContext(relative_path, self.agent) as context: original_content = context.get_original_content() if mode == "literal": regex = re.escape(needle) elif mode == "regex": regex = needle else: raise ValueError(f"Invalid mode: '{mode}', expected 'literal' or 'regex'.") # escape backslashes in repl repl = repl.replace("\\", "\\\\") # convert $!1, $!2, etc. to \1, \2, etc. for backreferences repl = re.sub(r"\$!(\d+)", "\\\\1", repl) # perform replacement updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE) if n == 0: return f"Error: No matches of search expression found in file '{relative_path}'." if not allow_multiple_occurrences and n > 1: return ( f"Error: Expression matches {n} occurrences in file '{relative_path}'. " "Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected." ) context.set_updated_content(updated_content) return SUCCESS_RESULT
- src/serena/agent.py:71-71 (registration)Legacy tool name mapping registers 'replace_regex' as an alias for the current name of ReplaceContentTool ("replace_content"), enabling backward compatibility in ToolSet handling.LEGACY_TOOL_NAME_MAPPING = {"replace_regex": ReplaceContentTool.get_name_from_cls()}
- src/serena/tools/tools_base.py:358-430 (registration)ToolRegistry automatically discovers and registers all subclasses of Tool in serena.tools modules, including ReplaceContentTool as 'replace_content', using get_name_from_cls().@singleton class ToolRegistry: def __init__(self) -> None: self._tool_dict: dict[str, RegisteredTool] = {} for cls in iter_subclasses(Tool): if not cls.__module__.startswith("serena.tools"): continue is_optional = issubclass(cls, ToolMarkerOptional) name = cls.get_name_from_cls() if name in self._tool_dict: raise ValueError(f"Duplicate tool name found: {name}. Tool classes must have unique names.") self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name) def get_tool_class_by_name(self, tool_name: str) -> type[Tool]: return self._tool_dict[tool_name].tool_class def get_all_tool_classes(self) -> list[type[Tool]]: return list(t.tool_class for t in self._tool_dict.values()) def get_tool_classes_default_enabled(self) -> list[type[Tool]]: """ :return: the list of tool classes that are enabled by default (i.e. non-optional tools). """ return [t.tool_class for t in self._tool_dict.values() if not t.is_optional] def get_tool_classes_optional(self) -> list[type[Tool]]: """ :return: the list of tool classes that are optional (i.e. disabled by default). """ return [t.tool_class for t in self._tool_dict.values() if t.is_optional] def get_tool_names_default_enabled(self) -> list[str]: """ :return: the list of tool names that are enabled by default (i.e. non-optional tools). """ return [t.tool_name for t in self._tool_dict.values() if not t.is_optional] def get_tool_names_optional(self) -> list[str]: """ :return: the list of tool names that are optional (i.e. disabled by default). """ return [t.tool_name for t in self._tool_dict.values() if t.is_optional] def get_tool_names(self) -> list[str]: """ :return: the list of all tool names. """ return list(self._tool_dict.keys()) def print_tool_overview( self, tools: Iterable[type[Tool] | Tool] | None = None, include_optional: bool = False, only_optional: bool = False ) -> None: """ Print a summary of the tools. If no tools are passed, a summary of the selection of tools (all, default or only optional) is printed. """ if tools is None: if only_optional: tools = self.get_tool_classes_optional() elif include_optional: tools = self.get_all_tool_classes() else: tools = self.get_tool_classes_default_enabled() tool_dict: dict[str, type[Tool] | Tool] = {} for tool_class in tools: tool_dict[tool_class.get_name_from_cls()] = tool_class for tool_name in sorted(tool_dict.keys()): tool_class = tool_dict[tool_name] print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}") def is_valid_tool_name(self, tool_name: str) -> bool: return tool_name in self._tool_dict
- The 'apply' method signature and docstring are used to generate the JSON schema and description for the MCP tool via func_metadata.def apply( self, relative_path: str, needle: str, repl: str, mode: Literal["literal", "regex"], allow_multiple_occurrences: bool = False, ) -> str: r""" Replaces one or more occurrences of a given pattern in a file with new content. This is the preferred way to replace content in a file whenever the symbol-level tools are not appropriate. VERY IMPORTANT: The "regex" mode allows very large sections of code to be replaced without fully quoting them! Use a regex of the form "beginning.*?end-of-text-to-be-replaced" to be faster and more economical! ALWAYS try to use wildcards to avoid specifying the exact content to be replaced, especially if it spans several lines. Note that you cannot make mistakes, because if the regex should match multiple occurrences while you disabled `allow_multiple_occurrences`, an error will be returned, and you can retry with a revised regex. Therefore, using regex mode with suitable wildcards is usually the best choice! :param relative_path: the relative path to the file :param needle: the string or regex pattern to search for. If `mode` is "literal", this string will be matched exactly. If `mode` is "regex", this string will be treated as a regular expression (syntax of Python's `re` module, with flags DOTALL and MULTILINE enabled). :param repl: the replacement string (verbatim). If mode is "regex", the string can contain backreferences to matched groups in the needle regex, specified using the syntax $!1, $!2, etc. for groups 1, 2, etc. :param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted. :param allow_multiple_occurrences: if True, the regex may match multiple occurrences in the file and all of them will be replaced. If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ return self.replace_content( relative_path, needle, repl, mode=mode, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True )