just-bash 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. just_bash-0.1.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,5 @@
1
+ """Rm command implementation."""
2
+
3
+ from .rm import RmCommand
4
+
5
+ __all__ = ["RmCommand"]
@@ -0,0 +1,106 @@
1
+ """Rm command implementation.
2
+
3
+ Usage: rm [OPTION]... FILE...
4
+
5
+ Remove (unlink) the FILE(s).
6
+
7
+ Options:
8
+ -f, --force ignore nonexistent files and arguments
9
+ -r, -R, --recursive remove directories and their contents recursively
10
+ -v, --verbose explain what is being done
11
+ """
12
+
13
+ from ...types import CommandContext, ExecResult
14
+
15
+
16
+ class RmCommand:
17
+ """The rm command."""
18
+
19
+ name = "rm"
20
+
21
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
22
+ """Execute the rm command."""
23
+ force = False
24
+ recursive = False
25
+ verbose = False
26
+ files: list[str] = []
27
+
28
+ # Parse arguments
29
+ i = 0
30
+ while i < len(args):
31
+ arg = args[i]
32
+ if arg == "--":
33
+ files.extend(args[i + 1:])
34
+ break
35
+ elif arg.startswith("--"):
36
+ if arg == "--force":
37
+ force = True
38
+ elif arg == "--recursive":
39
+ recursive = True
40
+ elif arg == "--verbose":
41
+ verbose = True
42
+ else:
43
+ return ExecResult(
44
+ stdout="",
45
+ stderr=f"rm: unrecognized option '{arg}'\n",
46
+ exit_code=1,
47
+ )
48
+ elif arg.startswith("-") and arg != "-":
49
+ for c in arg[1:]:
50
+ if c == "f":
51
+ force = True
52
+ elif c in ("r", "R"):
53
+ recursive = True
54
+ elif c == "v":
55
+ verbose = True
56
+ else:
57
+ return ExecResult(
58
+ stdout="",
59
+ stderr=f"rm: invalid option -- '{c}'\n",
60
+ exit_code=1,
61
+ )
62
+ else:
63
+ files.append(arg)
64
+ i += 1
65
+
66
+ if not files:
67
+ return ExecResult(
68
+ stdout="",
69
+ stderr="rm: missing operand\n",
70
+ exit_code=1,
71
+ )
72
+
73
+ stdout = ""
74
+ stderr = ""
75
+ exit_code = 0
76
+
77
+ for f in files:
78
+ try:
79
+ path = ctx.fs.resolve_path(ctx.cwd, f)
80
+
81
+ # Check if it's a directory
82
+ try:
83
+ st = await ctx.fs.stat(path)
84
+ if st.is_directory and not recursive:
85
+ stderr += f"rm: cannot remove '{f}': Is a directory\n"
86
+ exit_code = 1
87
+ continue
88
+ except FileNotFoundError:
89
+ if force:
90
+ continue
91
+ stderr += f"rm: cannot remove '{f}': No such file or directory\n"
92
+ exit_code = 1
93
+ continue
94
+
95
+ await ctx.fs.rm(path, recursive=recursive, force=force)
96
+ if verbose:
97
+ stdout += f"removed '{f}'\n"
98
+ except FileNotFoundError:
99
+ if not force:
100
+ stderr += f"rm: cannot remove '{f}': No such file or directory\n"
101
+ exit_code = 1
102
+ except OSError as e:
103
+ stderr += f"rm: cannot remove '{f}': {e}\n"
104
+ exit_code = 1
105
+
106
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,13 @@
1
+ """Search engine utility module for grep and rg commands."""
2
+
3
+ from .matcher import SearchOptions, SearchResult, search_content
4
+ from .regex import RegexMode, build_regex, convert_replacement
5
+
6
+ __all__ = [
7
+ "RegexMode",
8
+ "build_regex",
9
+ "convert_replacement",
10
+ "SearchOptions",
11
+ "SearchResult",
12
+ "search_content",
13
+ ]
@@ -0,0 +1,170 @@
1
+ """Content searching and matching utilities."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class SearchOptions:
9
+ """Options for content searching."""
10
+
11
+ invert_match: bool = False
12
+ show_line_numbers: bool = False
13
+ count_only: bool = False
14
+ only_matching: bool = False
15
+ filename: str | None = None
16
+ before_context: int = 0
17
+ after_context: int = 0
18
+ max_count: int | None = None
19
+ replace: str | None = None
20
+ multiline: bool = False
21
+
22
+
23
+ @dataclass
24
+ class SearchResult:
25
+ """Result of a content search."""
26
+
27
+ output: str
28
+ matched: bool
29
+ match_count: int
30
+
31
+
32
+ def search_content(
33
+ content: str,
34
+ regex: re.Pattern,
35
+ options: SearchOptions = None,
36
+ ) -> SearchResult:
37
+ """Search content and return formatted output.
38
+
39
+ Args:
40
+ content: The text content to search
41
+ regex: Compiled regex pattern to search for
42
+ options: Search options (defaults to SearchOptions())
43
+
44
+ Returns:
45
+ SearchResult with output, matched flag, and match count
46
+ """
47
+ if options is None:
48
+ options = SearchOptions()
49
+
50
+ lines = content.split("\n")
51
+ # Handle trailing empty line from split
52
+ if lines and lines[-1] == "":
53
+ lines = lines[:-1]
54
+
55
+ # Track matches
56
+ matches: list[tuple[int, str, list[re.Match]]] = []
57
+ match_count = 0
58
+
59
+ # Find all matching lines
60
+ for line_num, line in enumerate(lines, 1):
61
+ line_matches = list(regex.finditer(line))
62
+ is_match = bool(line_matches)
63
+
64
+ if options.invert_match:
65
+ is_match = not is_match
66
+
67
+ if is_match:
68
+ if options.invert_match:
69
+ matches.append((line_num, line, []))
70
+ else:
71
+ matches.append((line_num, line, line_matches))
72
+ match_count += len(line_matches)
73
+
74
+ # Check max count
75
+ if options.max_count is not None and len(matches) >= options.max_count:
76
+ break
77
+
78
+ if not matches:
79
+ return SearchResult(output="", matched=False, match_count=0)
80
+
81
+ # For count_only, just return the count info
82
+ if options.count_only:
83
+ return SearchResult(
84
+ output=str(len(matches)),
85
+ matched=True,
86
+ match_count=match_count,
87
+ )
88
+
89
+ # Build output with context
90
+ output_lines: list[str] = []
91
+
92
+ if options.before_context > 0 or options.after_context > 0:
93
+ # Collect all lines to output (including context)
94
+ lines_to_show: dict[int, tuple[str, bool]] = {} # line_num -> (content, is_match)
95
+
96
+ for line_num, line, line_matches in matches:
97
+ # Add context before
98
+ for ctx_num in range(max(1, line_num - options.before_context), line_num):
99
+ if ctx_num not in lines_to_show:
100
+ lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
101
+ # Add the match line
102
+ lines_to_show[line_num] = (line, True)
103
+ # Add context after
104
+ end_ctx = min(len(lines) + 1, line_num + options.after_context + 1)
105
+ for ctx_num in range(line_num + 1, end_ctx):
106
+ if ctx_num not in lines_to_show:
107
+ lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
108
+
109
+ # Output in order
110
+ for line_num in sorted(lines_to_show.keys()):
111
+ line_content, is_match = lines_to_show[line_num]
112
+ sep = ":" if is_match else "-"
113
+
114
+ if options.only_matching and is_match:
115
+ # For context lines with only_matching, we don't output them
116
+ # But for simplicity, include all for now
117
+ pass
118
+
119
+ output_line = _format_line(
120
+ line_num, line_content, options, sep=sep
121
+ )
122
+ output_lines.append(output_line)
123
+ else:
124
+ # Normal output without context
125
+ for line_num, line, line_matches in matches:
126
+ if options.only_matching and line_matches:
127
+ # Output each match separately
128
+ for m in line_matches:
129
+ output_line = _format_line(
130
+ line_num, m.group(0), options
131
+ )
132
+ output_lines.append(output_line)
133
+ else:
134
+ # Handle replacement
135
+ output_text = line
136
+ if options.replace is not None and line_matches:
137
+ output_text = regex.sub(options.replace, line)
138
+
139
+ output_line = _format_line(line_num, output_text, options)
140
+ output_lines.append(output_line)
141
+
142
+ output = "\n".join(output_lines)
143
+ if output:
144
+ output += "\n"
145
+
146
+ return SearchResult(
147
+ output=output,
148
+ matched=True,
149
+ match_count=match_count,
150
+ )
151
+
152
+
153
+ def _format_line(
154
+ line_num: int,
155
+ content: str,
156
+ options: SearchOptions,
157
+ sep: str = ":",
158
+ ) -> str:
159
+ """Format a single output line with optional prefix."""
160
+ parts = []
161
+
162
+ if options.filename:
163
+ parts.append(f"{options.filename}{sep}")
164
+
165
+ if options.show_line_numbers:
166
+ parts.append(f"{line_num}{sep}")
167
+
168
+ parts.append(content)
169
+
170
+ return "".join(parts)
@@ -0,0 +1,159 @@
1
+ """Regex pattern building utilities for search commands."""
2
+
3
+ import re
4
+ from enum import Enum
5
+
6
+
7
+ class RegexMode(Enum):
8
+ """Regex interpretation modes."""
9
+
10
+ BASIC = "basic" # BRE - escape +?|(){}
11
+ EXTENDED = "extended" # ERE - standard regex
12
+ FIXED = "fixed" # Literal string matching
13
+ PERL = "perl" # PCRE (Python default)
14
+
15
+
16
+ def build_regex(
17
+ pattern: str,
18
+ mode: RegexMode = RegexMode.EXTENDED,
19
+ ignore_case: bool = False,
20
+ whole_word: bool = False,
21
+ line_regexp: bool = False,
22
+ multiline: bool = False,
23
+ ) -> re.Pattern:
24
+ """Build a compiled regex from pattern with options.
25
+
26
+ Args:
27
+ pattern: The search pattern
28
+ mode: How to interpret the pattern (basic, extended, fixed, perl)
29
+ ignore_case: Case-insensitive matching
30
+ whole_word: Match whole words only (add word boundaries)
31
+ line_regexp: Match entire lines (anchor to line start/end)
32
+ multiline: Enable multiline mode for ^ and $
33
+
34
+ Returns:
35
+ Compiled regex pattern
36
+ """
37
+ # Process pattern based on mode
38
+ if mode == RegexMode.FIXED:
39
+ # Escape all regex metacharacters for literal matching
40
+ pattern = re.escape(pattern)
41
+ elif mode == RegexMode.BASIC:
42
+ # BRE mode: +?|(){} are literal unless escaped
43
+ # Convert BRE to ERE by escaping these chars
44
+ # In BRE, \+ means one-or-more, + means literal +
45
+ # We need to swap the meaning
46
+ pattern = _convert_bre_to_ere(pattern)
47
+ # EXTENDED and PERL modes use pattern as-is (Python re is PCRE-like)
48
+
49
+ # Apply word boundaries
50
+ if whole_word:
51
+ pattern = r"\b(?:" + pattern + r")\b"
52
+
53
+ # Apply line anchors
54
+ if line_regexp:
55
+ pattern = "^(?:" + pattern + ")$"
56
+
57
+ # Build flags
58
+ flags = 0
59
+ if ignore_case:
60
+ flags |= re.IGNORECASE
61
+ if multiline:
62
+ flags |= re.MULTILINE
63
+
64
+ return re.compile(pattern, flags)
65
+
66
+
67
+ def _convert_bre_to_ere(pattern: str) -> str:
68
+ """Convert Basic Regular Expression to Extended Regular Expression.
69
+
70
+ In BRE, characters like +, ?, |, (, ), {, } are literal unless escaped.
71
+ In ERE (Python's default), they are special unless escaped.
72
+
73
+ This function escapes these characters so they're treated as literals.
74
+ """
75
+ # Characters that are special in ERE but literal in BRE
76
+ literal_chars = "+?|(){}[]"
77
+
78
+ result = []
79
+ i = 0
80
+ while i < len(pattern):
81
+ char = pattern[i]
82
+
83
+ if char == "\\" and i + 1 < len(pattern):
84
+ next_char = pattern[i + 1]
85
+ # In BRE, \+ means one-or-more (special)
86
+ # So we should NOT escape it (keep as +)
87
+ if next_char in literal_chars:
88
+ result.append(next_char)
89
+ i += 2
90
+ continue
91
+ else:
92
+ # Keep other escape sequences as-is
93
+ result.append(char)
94
+ result.append(next_char)
95
+ i += 2
96
+ continue
97
+ elif char in literal_chars:
98
+ # Literal in BRE, so escape for ERE
99
+ result.append("\\" + char)
100
+ else:
101
+ result.append(char)
102
+ i += 1
103
+
104
+ return "".join(result)
105
+
106
+
107
+ def convert_replacement(replacement: str) -> str:
108
+ """Convert sed/ripgrep-style replacement to Python re.sub format.
109
+
110
+ Converts:
111
+ $0 -> \\g<0> (full match)
112
+ $1, $2, ... -> \\1, \\2, ... (numbered groups)
113
+ $name -> \\g<name> (named groups)
114
+
115
+ Args:
116
+ replacement: The replacement string with $ references
117
+
118
+ Returns:
119
+ Replacement string with Python-style backreferences
120
+ """
121
+ result = []
122
+ i = 0
123
+ while i < len(replacement):
124
+ char = replacement[i]
125
+
126
+ if char == "$" and i + 1 < len(replacement):
127
+ next_char = replacement[i + 1]
128
+
129
+ # $0 - full match (needs \g<0> syntax)
130
+ if next_char == "0":
131
+ result.append(r"\g<0>")
132
+ i += 2
133
+ continue
134
+
135
+ # $1-9 - numbered groups
136
+ if next_char.isdigit():
137
+ # Collect all digits
138
+ j = i + 1
139
+ while j < len(replacement) and replacement[j].isdigit():
140
+ j += 1
141
+ num = replacement[i + 1:j]
142
+ result.append("\\" + num)
143
+ i = j
144
+ continue
145
+
146
+ # $name - named groups
147
+ if next_char.isalpha() or next_char == "_":
148
+ j = i + 1
149
+ while j < len(replacement) and (replacement[j].isalnum() or replacement[j] == "_"):
150
+ j += 1
151
+ name = replacement[i + 1:j]
152
+ result.append(r"\g<" + name + ">")
153
+ i = j
154
+ continue
155
+
156
+ result.append(char)
157
+ i += 1
158
+
159
+ return "".join(result)
@@ -0,0 +1,5 @@
1
+ """Sed command implementation."""
2
+
3
+ from .sed import SedCommand
4
+
5
+ __all__ = ["SedCommand"]