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
+ """Tee command."""
2
+
3
+ from .tee import TeeCommand
4
+
5
+ __all__ = ["TeeCommand"]
@@ -0,0 +1,63 @@
1
+ """Tee command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class TeeCommand:
7
+ """The tee command."""
8
+
9
+ name = "tee"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the tee command."""
13
+ append = False
14
+ files: list[str] = []
15
+
16
+ i = 0
17
+ while i < len(args):
18
+ arg = args[i]
19
+ if arg == "--":
20
+ files.extend(args[i + 1:])
21
+ break
22
+ elif arg in ("-a", "--append"):
23
+ append = True
24
+ elif arg == "--help":
25
+ return ExecResult(
26
+ stdout="Usage: tee [OPTION]... [FILE]...\n",
27
+ stderr="",
28
+ exit_code=0,
29
+ )
30
+ elif arg.startswith("-") and len(arg) > 1:
31
+ return ExecResult(
32
+ stdout="",
33
+ stderr=f"tee: invalid option -- '{arg[1]}'\n",
34
+ exit_code=1,
35
+ )
36
+ else:
37
+ files.append(arg)
38
+ i += 1
39
+
40
+ # Read stdin
41
+ content = ctx.stdin
42
+
43
+ # Write to files
44
+ stderr = ""
45
+ exit_code = 0
46
+
47
+ for f in files:
48
+ try:
49
+ path = ctx.fs.resolve_path(ctx.cwd, f)
50
+ if append:
51
+ try:
52
+ existing = await ctx.fs.read_file(path)
53
+ await ctx.fs.write_file(path, existing + content)
54
+ except FileNotFoundError:
55
+ await ctx.fs.write_file(path, content)
56
+ else:
57
+ await ctx.fs.write_file(path, content)
58
+ except Exception as e:
59
+ stderr += f"tee: {f}: {e}\n"
60
+ exit_code = 1
61
+
62
+ # Output to stdout
63
+ return ExecResult(stdout=content, stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,5 @@
1
+ """Timeout command."""
2
+
3
+ from .timeout import TimeoutCommand
4
+
5
+ __all__ = ["TimeoutCommand"]
@@ -0,0 +1,188 @@
1
+ """Timeout command implementation."""
2
+
3
+ import asyncio
4
+ import re
5
+ from ...types import CommandContext, ExecResult
6
+
7
+
8
+ class TimeoutCommand:
9
+ """The timeout command."""
10
+
11
+ name = "timeout"
12
+
13
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
14
+ """Execute the timeout command."""
15
+ duration = None
16
+ kill_after = None
17
+ signal = "TERM"
18
+ preserve_status = False
19
+ command: list[str] = []
20
+
21
+ i = 0
22
+ while i < len(args):
23
+ arg = args[i]
24
+ if arg == "-k" and i + 1 < len(args):
25
+ i += 1
26
+ try:
27
+ kill_after = self._parse_duration(args[i])
28
+ except ValueError:
29
+ return ExecResult(
30
+ stdout="",
31
+ stderr=f"timeout: invalid time interval '{args[i]}'\n",
32
+ exit_code=1,
33
+ )
34
+ elif arg.startswith("-k"):
35
+ try:
36
+ kill_after = self._parse_duration(arg[2:])
37
+ except ValueError:
38
+ return ExecResult(
39
+ stdout="",
40
+ stderr=f"timeout: invalid time interval '{arg[2:]}'\n",
41
+ exit_code=1,
42
+ )
43
+ elif arg.startswith("--kill-after="):
44
+ try:
45
+ kill_after = self._parse_duration(arg[13:])
46
+ except ValueError:
47
+ return ExecResult(
48
+ stdout="",
49
+ stderr=f"timeout: invalid time interval '{arg[13:]}'\n",
50
+ exit_code=1,
51
+ )
52
+ elif arg == "--kill-after" and i + 1 < len(args):
53
+ i += 1
54
+ try:
55
+ kill_after = self._parse_duration(args[i])
56
+ except ValueError:
57
+ return ExecResult(
58
+ stdout="",
59
+ stderr=f"timeout: invalid time interval '{args[i]}'\n",
60
+ exit_code=1,
61
+ )
62
+ elif arg == "-s" and i + 1 < len(args):
63
+ i += 1
64
+ signal = args[i]
65
+ elif arg.startswith("-s"):
66
+ signal = arg[2:]
67
+ elif arg.startswith("--signal="):
68
+ signal = arg[9:]
69
+ elif arg == "--signal" and i + 1 < len(args):
70
+ i += 1
71
+ signal = args[i]
72
+ elif arg == "--preserve-status":
73
+ preserve_status = True
74
+ elif arg == "--foreground":
75
+ pass # Ignore
76
+ elif arg == "--help":
77
+ return ExecResult(
78
+ stdout=(
79
+ "Usage: timeout [OPTION] DURATION COMMAND [ARG]...\n"
80
+ "Start COMMAND, and kill it if still running after DURATION.\n\n"
81
+ "Options:\n"
82
+ " -k, --kill-after=DURATION send KILL signal after DURATION\n"
83
+ " -s, --signal=SIGNAL send this signal on timeout (default: TERM)\n"
84
+ " --preserve-status exit with the same status as COMMAND\n"
85
+ " --foreground run command in foreground\n"
86
+ " --help display this help and exit\n"
87
+ ),
88
+ stderr="",
89
+ exit_code=0,
90
+ )
91
+ elif arg == "--":
92
+ # Rest are command args
93
+ if duration is None and i + 1 < len(args):
94
+ i += 1
95
+ try:
96
+ duration = self._parse_duration(args[i])
97
+ except ValueError:
98
+ return ExecResult(
99
+ stdout="",
100
+ stderr=f"timeout: invalid duration '{args[i]}'\n",
101
+ exit_code=1,
102
+ )
103
+ if i + 1 < len(args):
104
+ command = args[i + 1:]
105
+ break
106
+ elif arg.startswith("-") and len(arg) > 1 and not arg[1].isdigit():
107
+ return ExecResult(
108
+ stdout="",
109
+ stderr=f"timeout: invalid option -- '{arg[1]}'\n",
110
+ exit_code=1,
111
+ )
112
+ elif duration is None:
113
+ try:
114
+ duration = self._parse_duration(arg)
115
+ except ValueError:
116
+ return ExecResult(
117
+ stdout="",
118
+ stderr=f"timeout: invalid duration '{arg}'\n",
119
+ exit_code=1,
120
+ )
121
+ else:
122
+ command = args[i:]
123
+ break
124
+ i += 1
125
+
126
+ if duration is None:
127
+ return ExecResult(
128
+ stdout="",
129
+ stderr="timeout: missing operand\n",
130
+ exit_code=1,
131
+ )
132
+
133
+ if not command:
134
+ return ExecResult(
135
+ stdout="",
136
+ stderr="timeout: missing command\n",
137
+ exit_code=1,
138
+ )
139
+
140
+ # Quote arguments properly for shell execution
141
+ def quote(s: str) -> str:
142
+ if not s or any(c in s for c in " \t\n'\"\\$`!"):
143
+ return "'" + s.replace("'", "'\"'\"'") + "'"
144
+ return s
145
+
146
+ cmd_str = " ".join(quote(c) for c in command)
147
+
148
+ # Execute command with timeout
149
+ if not ctx.exec:
150
+ return ExecResult(
151
+ stdout="",
152
+ stderr="timeout: cannot execute commands\n",
153
+ exit_code=126,
154
+ )
155
+
156
+ try:
157
+ result = await asyncio.wait_for(
158
+ ctx.exec(cmd_str, {"cwd": ctx.cwd}),
159
+ timeout=duration,
160
+ )
161
+ return result
162
+ except asyncio.TimeoutError:
163
+ # In a sandboxed environment, we can't actually send signals
164
+ # The -k and -s options are parsed but have limited effect
165
+ # Return 124 unless preserve_status is set (then we can't know the status)
166
+ if preserve_status:
167
+ # In real timeout, this would preserve the signal exit status
168
+ # We return 124 + signal number approximation
169
+ return ExecResult(stdout="", stderr="", exit_code=124)
170
+ return ExecResult(stdout="", stderr="", exit_code=124)
171
+
172
+ def _parse_duration(self, s: str) -> float:
173
+ """Parse a duration string."""
174
+ match = re.match(r"^(\d+(?:\.\d+)?)(s|m|h|d)?$", s)
175
+ if not match:
176
+ raise ValueError(f"Invalid duration: {s}")
177
+
178
+ value = float(match.group(1))
179
+ suffix = match.group(2)
180
+
181
+ if suffix == "m":
182
+ value *= 60
183
+ elif suffix == "h":
184
+ value *= 3600
185
+ elif suffix == "d":
186
+ value *= 86400
187
+
188
+ return value
@@ -0,0 +1,5 @@
1
+ """Touch command implementation."""
2
+
3
+ from .touch import TouchCommand
4
+
5
+ __all__ = ["TouchCommand"]
@@ -0,0 +1,91 @@
1
+ """Touch command implementation.
2
+
3
+ Usage: touch [OPTION]... FILE...
4
+
5
+ Update the access and modification times of each FILE to the current time.
6
+ A FILE argument that does not exist is created empty.
7
+
8
+ Options:
9
+ -c, --no-create do not create any files
10
+ """
11
+
12
+ from ...types import CommandContext, ExecResult
13
+
14
+
15
+ class TouchCommand:
16
+ """The touch command."""
17
+
18
+ name = "touch"
19
+
20
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
21
+ """Execute the touch command."""
22
+ no_create = False
23
+ files: list[str] = []
24
+
25
+ # Parse arguments
26
+ i = 0
27
+ while i < len(args):
28
+ arg = args[i]
29
+ if arg == "--":
30
+ files.extend(args[i + 1:])
31
+ break
32
+ elif arg.startswith("--"):
33
+ if arg == "--no-create":
34
+ no_create = True
35
+ else:
36
+ return ExecResult(
37
+ stdout="",
38
+ stderr=f"touch: unrecognized option '{arg}'\n",
39
+ exit_code=1,
40
+ )
41
+ elif arg.startswith("-") and arg != "-":
42
+ for c in arg[1:]:
43
+ if c == "c":
44
+ no_create = True
45
+ else:
46
+ return ExecResult(
47
+ stdout="",
48
+ stderr=f"touch: invalid option -- '{c}'\n",
49
+ exit_code=1,
50
+ )
51
+ else:
52
+ files.append(arg)
53
+ i += 1
54
+
55
+ if not files:
56
+ return ExecResult(
57
+ stdout="",
58
+ stderr="touch: missing file operand\n",
59
+ exit_code=1,
60
+ )
61
+
62
+ stderr = ""
63
+ exit_code = 0
64
+
65
+ for f in files:
66
+ try:
67
+ path = ctx.fs.resolve_path(ctx.cwd, f)
68
+ # Check if file exists
69
+ try:
70
+ stat = await ctx.fs.stat(path)
71
+ if stat.is_directory:
72
+ # Touching a directory - we can't easily update dir mtime
73
+ # in current implementation, so just continue
74
+ continue
75
+ # File exists - read and re-write to update timestamp
76
+ content = await ctx.fs.read_file(path)
77
+ await ctx.fs.write_file(path, content)
78
+ except FileNotFoundError:
79
+ # File doesn't exist
80
+ if no_create:
81
+ continue
82
+ # Create empty file
83
+ await ctx.fs.write_file(path, "")
84
+ except FileNotFoundError:
85
+ stderr += f"touch: cannot touch '{f}': No such file or directory\n"
86
+ exit_code = 1
87
+ except IsADirectoryError:
88
+ # Touching a directory is fine, just update timestamp (no-op)
89
+ pass
90
+
91
+ return ExecResult(stdout="", stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,5 @@
1
+ """Tr command implementation."""
2
+
3
+ from .tr import TrCommand
4
+
5
+ __all__ = ["TrCommand"]
@@ -0,0 +1,297 @@
1
+ """Tr command implementation.
2
+
3
+ Usage: tr [OPTION]... SET1 [SET2]
4
+
5
+ Translate, squeeze, and/or delete characters from standard input,
6
+ writing to standard output.
7
+
8
+ Options:
9
+ -c, -C, --complement use the complement of SET1
10
+ -d, --delete delete characters in SET1, do not translate
11
+ -s, --squeeze-repeats replace each sequence of a repeated character
12
+ that is listed in SET1 with a single occurrence
13
+
14
+ SETs are specified as strings of characters. Interpreted sequences include:
15
+ \\NNN character with octal value NNN (1 to 3 octal digits)
16
+ \\\\ backslash
17
+ \\a audible BEL
18
+ \\b backspace
19
+ \\f form feed
20
+ \\n newline
21
+ \\r carriage return
22
+ \\t horizontal tab
23
+ \\v vertical tab
24
+ CHAR1-CHAR2 all characters from CHAR1 to CHAR2 in ascending order
25
+ [:alnum:] all letters and digits
26
+ [:alpha:] all letters
27
+ [:blank:] all horizontal whitespace
28
+ [:cntrl:] all control characters
29
+ [:digit:] all digits
30
+ [:lower:] all lowercase letters
31
+ [:print:] all printable characters
32
+ [:punct:] all punctuation characters
33
+ [:space:] all horizontal or vertical whitespace
34
+ [:upper:] all uppercase letters
35
+ """
36
+
37
+ import string
38
+ from ...types import CommandContext, ExecResult
39
+
40
+
41
+ class TrCommand:
42
+ """The tr command."""
43
+
44
+ name = "tr"
45
+
46
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
47
+ """Execute the tr command."""
48
+ complement = False
49
+ delete = False
50
+ squeeze = False
51
+ sets: list[str] = []
52
+
53
+ # Parse arguments
54
+ i = 0
55
+ while i < len(args):
56
+ arg = args[i]
57
+ if arg == "--":
58
+ sets.extend(args[i + 1:])
59
+ break
60
+ elif arg.startswith("--"):
61
+ if arg == "--complement":
62
+ complement = True
63
+ elif arg == "--delete":
64
+ delete = True
65
+ elif arg == "--squeeze-repeats":
66
+ squeeze = True
67
+ else:
68
+ return ExecResult(
69
+ stdout="",
70
+ stderr=f"tr: unrecognized option '{arg}'\n",
71
+ exit_code=1,
72
+ )
73
+ elif arg.startswith("-") and arg != "-":
74
+ j = 1
75
+ while j < len(arg):
76
+ c = arg[j]
77
+ if c == "c" or c == "C":
78
+ complement = True
79
+ elif c == "d":
80
+ delete = True
81
+ elif c == "s":
82
+ squeeze = True
83
+ else:
84
+ return ExecResult(
85
+ stdout="",
86
+ stderr=f"tr: invalid option -- '{c}'\n",
87
+ exit_code=1,
88
+ )
89
+ j += 1
90
+ else:
91
+ sets.append(arg)
92
+ i += 1
93
+
94
+ # Validate arguments
95
+ if delete:
96
+ if len(sets) < 1:
97
+ return ExecResult(
98
+ stdout="",
99
+ stderr="tr: missing operand\n",
100
+ exit_code=1,
101
+ )
102
+ if len(sets) > 1 and not squeeze:
103
+ return ExecResult(
104
+ stdout="",
105
+ stderr="tr: extra operand\n",
106
+ exit_code=1,
107
+ )
108
+ else:
109
+ if len(sets) < 2 and not squeeze:
110
+ return ExecResult(
111
+ stdout="",
112
+ stderr="tr: missing operand after SET1\n",
113
+ exit_code=1,
114
+ )
115
+
116
+ # Expand sets
117
+ try:
118
+ set1 = self._expand_set(sets[0]) if sets else ""
119
+ set2 = self._expand_set(sets[1]) if len(sets) > 1 else ""
120
+ except ValueError as e:
121
+ return ExecResult(
122
+ stdout="",
123
+ stderr=f"tr: {e}\n",
124
+ exit_code=1,
125
+ )
126
+
127
+ # Apply complement
128
+ if complement:
129
+ # Create set of all characters not in set1
130
+ all_chars = set(chr(i) for i in range(256))
131
+ set1_chars = set(set1)
132
+ set1 = "".join(sorted(all_chars - set1_chars, key=ord))
133
+
134
+ # Process input
135
+ content = ctx.stdin
136
+ result = []
137
+
138
+ if delete and not squeeze:
139
+ # Delete characters in set1
140
+ delete_set = set(set1)
141
+ for c in content:
142
+ if c not in delete_set:
143
+ result.append(c)
144
+ elif delete and squeeze:
145
+ # Delete set1, squeeze set2
146
+ delete_set = set(set1)
147
+ squeeze_set = set(set2)
148
+ prev_char = None
149
+ for c in content:
150
+ if c in delete_set:
151
+ continue
152
+ if c in squeeze_set and c == prev_char:
153
+ continue
154
+ result.append(c)
155
+ prev_char = c
156
+ elif squeeze and not set2:
157
+ # Squeeze only
158
+ squeeze_set = set(set1)
159
+ prev_char = None
160
+ for c in content:
161
+ if c in squeeze_set and c == prev_char:
162
+ continue
163
+ result.append(c)
164
+ prev_char = c
165
+ else:
166
+ # Translate
167
+ trans_map = self._create_translation_map(set1, set2)
168
+
169
+ if squeeze:
170
+ squeeze_set = set(set2)
171
+ prev_char = None
172
+ for c in content:
173
+ translated = trans_map.get(c, c)
174
+ if translated in squeeze_set and translated == prev_char:
175
+ continue
176
+ result.append(translated)
177
+ prev_char = translated
178
+ else:
179
+ for c in content:
180
+ result.append(trans_map.get(c, c))
181
+
182
+ return ExecResult(stdout="".join(result), stderr="", exit_code=0)
183
+
184
+ def _expand_set(self, s: str) -> str:
185
+ """Expand a character set specification."""
186
+ result = []
187
+ i = 0
188
+
189
+ while i < len(s):
190
+ # Check for character classes like [:digit:]
191
+ if s[i:].startswith("[:") and ":]" in s[i + 2:]:
192
+ end = s.index(":]", i + 2)
193
+ class_name = s[i + 2:end]
194
+ result.extend(self._get_char_class(class_name))
195
+ i = end + 2
196
+ continue
197
+
198
+ # Check for escape sequences
199
+ if s[i] == "\\" and i + 1 < len(s):
200
+ c = s[i + 1]
201
+ if c == "a":
202
+ result.append("\a")
203
+ elif c == "b":
204
+ result.append("\b")
205
+ elif c == "f":
206
+ result.append("\f")
207
+ elif c == "n":
208
+ result.append("\n")
209
+ elif c == "r":
210
+ result.append("\r")
211
+ elif c == "t":
212
+ result.append("\t")
213
+ elif c == "v":
214
+ result.append("\v")
215
+ elif c == "\\":
216
+ result.append("\\")
217
+ elif c.isdigit():
218
+ # Octal escape
219
+ octal = ""
220
+ j = i + 1
221
+ while j < len(s) and s[j].isdigit() and len(octal) < 3:
222
+ if s[j] in "01234567":
223
+ octal += s[j]
224
+ j += 1
225
+ else:
226
+ break
227
+ if octal:
228
+ result.append(chr(int(octal, 8)))
229
+ i = j
230
+ continue
231
+ else:
232
+ result.append(c)
233
+ else:
234
+ result.append(c)
235
+ i += 2
236
+ continue
237
+
238
+ # Check for range
239
+ if i + 2 < len(s) and s[i + 1] == "-":
240
+ start = s[i]
241
+ end = s[i + 2]
242
+ if ord(start) <= ord(end):
243
+ for c in range(ord(start), ord(end) + 1):
244
+ result.append(chr(c))
245
+ i += 3
246
+ continue
247
+
248
+ result.append(s[i])
249
+ i += 1
250
+
251
+ return "".join(result)
252
+
253
+ def _get_char_class(self, name: str) -> list[str]:
254
+ """Get characters in a character class."""
255
+ if name == "alnum":
256
+ return list(string.ascii_letters + string.digits)
257
+ elif name == "alpha":
258
+ return list(string.ascii_letters)
259
+ elif name == "blank":
260
+ return [" ", "\t"]
261
+ elif name == "cntrl":
262
+ return [chr(i) for i in range(32)] + [chr(127)]
263
+ elif name == "digit":
264
+ return list(string.digits)
265
+ elif name == "graph":
266
+ return [chr(i) for i in range(33, 127)]
267
+ elif name == "lower":
268
+ return list(string.ascii_lowercase)
269
+ elif name == "print":
270
+ return [chr(i) for i in range(32, 127)]
271
+ elif name == "punct":
272
+ return list(string.punctuation)
273
+ elif name == "space":
274
+ return list(string.whitespace)
275
+ elif name == "upper":
276
+ return list(string.ascii_uppercase)
277
+ elif name == "xdigit":
278
+ return list(string.hexdigits)
279
+ else:
280
+ raise ValueError(f"invalid character class '{name}'")
281
+
282
+ def _create_translation_map(self, set1: str, set2: str) -> dict[str, str]:
283
+ """Create a translation map from set1 to set2."""
284
+ trans_map = {}
285
+
286
+ # If set2 is shorter, extend with its last character
287
+ if set2:
288
+ last_char = set2[-1]
289
+ set2_extended = set2 + last_char * (len(set1) - len(set2))
290
+ else:
291
+ set2_extended = ""
292
+
293
+ for i, c in enumerate(set1):
294
+ if i < len(set2_extended):
295
+ trans_map[c] = set2_extended[i]
296
+
297
+ return trans_map
@@ -0,0 +1,5 @@
1
+ """Tree command."""
2
+
3
+ from .tree import TreeCommand
4
+
5
+ __all__ = ["TreeCommand"]