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,118 @@
1
+ """Diff command implementation."""
2
+
3
+ import difflib
4
+ from ...types import CommandContext, ExecResult
5
+
6
+
7
+ class DiffCommand:
8
+ """The diff command."""
9
+
10
+ name = "diff"
11
+
12
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
13
+ """Execute the diff command."""
14
+ brief = False
15
+ report_identical = False
16
+ ignore_case = False
17
+ files: list[str] = []
18
+
19
+ i = 0
20
+ while i < len(args):
21
+ arg = args[i]
22
+ if arg in ("-q", "--brief"):
23
+ brief = True
24
+ elif arg in ("-s", "--report-identical-files"):
25
+ report_identical = True
26
+ elif arg in ("-i", "--ignore-case"):
27
+ ignore_case = True
28
+ elif arg == "--help":
29
+ return ExecResult(
30
+ stdout="Usage: diff [OPTION]... FILES\n",
31
+ stderr="",
32
+ exit_code=0,
33
+ )
34
+ elif arg == "--":
35
+ files.extend(args[i + 1:])
36
+ break
37
+ elif arg.startswith("-") and len(arg) > 1 and arg != "-":
38
+ return ExecResult(
39
+ stdout="",
40
+ stderr=f"diff: invalid option -- '{arg[1]}'\n",
41
+ exit_code=1,
42
+ )
43
+ else:
44
+ files.append(arg)
45
+ i += 1
46
+
47
+ if len(files) < 2:
48
+ return ExecResult(
49
+ stdout="",
50
+ stderr="diff: missing operand\n",
51
+ exit_code=2,
52
+ )
53
+
54
+ file1, file2 = files[0], files[1]
55
+
56
+ # Read files
57
+ try:
58
+ if file1 == "-":
59
+ content1 = ctx.stdin
60
+ else:
61
+ path1 = ctx.fs.resolve_path(ctx.cwd, file1)
62
+ content1 = await ctx.fs.read_file(path1)
63
+ except FileNotFoundError:
64
+ return ExecResult(
65
+ stdout="",
66
+ stderr=f"diff: {file1}: No such file or directory\n",
67
+ exit_code=2,
68
+ )
69
+
70
+ try:
71
+ if file2 == "-":
72
+ content2 = ctx.stdin
73
+ else:
74
+ path2 = ctx.fs.resolve_path(ctx.cwd, file2)
75
+ content2 = await ctx.fs.read_file(path2)
76
+ except FileNotFoundError:
77
+ return ExecResult(
78
+ stdout="",
79
+ stderr=f"diff: {file2}: No such file or directory\n",
80
+ exit_code=2,
81
+ )
82
+
83
+ # Compare
84
+ if ignore_case:
85
+ compare1 = content1.lower()
86
+ compare2 = content2.lower()
87
+ else:
88
+ compare1 = content1
89
+ compare2 = content2
90
+
91
+ if compare1 == compare2:
92
+ if report_identical:
93
+ return ExecResult(
94
+ stdout=f"Files {file1} and {file2} are identical\n",
95
+ stderr="",
96
+ exit_code=0,
97
+ )
98
+ return ExecResult(stdout="", stderr="", exit_code=0)
99
+
100
+ # Files differ
101
+ if brief:
102
+ return ExecResult(
103
+ stdout=f"Files {file1} and {file2} differ\n",
104
+ stderr="",
105
+ exit_code=1,
106
+ )
107
+
108
+ # Generate unified diff
109
+ lines1 = content1.splitlines(keepends=True)
110
+ lines2 = content2.splitlines(keepends=True)
111
+
112
+ diff = difflib.unified_diff(
113
+ lines1, lines2,
114
+ fromfile=file1, tofile=file2,
115
+ )
116
+
117
+ output = "".join(diff)
118
+ return ExecResult(stdout=output, stderr="", exit_code=1)
@@ -0,0 +1,5 @@
1
+ """Dirname command."""
2
+
3
+ from .dirname import DirnameCommand
4
+
5
+ __all__ = ["DirnameCommand"]
@@ -0,0 +1,56 @@
1
+ """Dirname command implementation."""
2
+
3
+ import os
4
+ from ...types import CommandContext, ExecResult
5
+
6
+
7
+ class DirnameCommand:
8
+ """The dirname command."""
9
+
10
+ name = "dirname"
11
+
12
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
13
+ """Execute the dirname command."""
14
+ paths: list[str] = []
15
+
16
+ i = 0
17
+ while i < len(args):
18
+ arg = args[i]
19
+ if arg == "--help":
20
+ return ExecResult(
21
+ stdout="Usage: dirname [OPTION] NAME...\n",
22
+ stderr="",
23
+ exit_code=0,
24
+ )
25
+ elif arg == "--":
26
+ paths.extend(args[i + 1:])
27
+ break
28
+ elif arg.startswith("-") and len(arg) > 1:
29
+ return ExecResult(
30
+ stdout="",
31
+ stderr=f"dirname: invalid option -- '{arg[1]}'\n",
32
+ exit_code=1,
33
+ )
34
+ else:
35
+ paths.append(arg)
36
+ i += 1
37
+
38
+ if not paths:
39
+ return ExecResult(
40
+ stdout="",
41
+ stderr="dirname: missing operand\n",
42
+ exit_code=1,
43
+ )
44
+
45
+ results = []
46
+ for path in paths:
47
+ dirname = os.path.dirname(path.rstrip("/"))
48
+ if not dirname:
49
+ dirname = "."
50
+ results.append(dirname)
51
+
52
+ return ExecResult(
53
+ stdout="\n".join(results) + "\n",
54
+ stderr="",
55
+ exit_code=0,
56
+ )
@@ -0,0 +1,5 @@
1
+ """Du command."""
2
+
3
+ from .du import DuCommand
4
+
5
+ __all__ = ["DuCommand"]
@@ -0,0 +1,150 @@
1
+ """Du command implementation - disk usage."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class DuCommand:
7
+ """The du command - disk usage."""
8
+
9
+ name = "du"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the du command."""
13
+ show_all = False
14
+ summary_only = False
15
+ human_readable = False
16
+ show_total = False
17
+ max_depth = None
18
+ paths: list[str] = []
19
+
20
+ i = 0
21
+ while i < len(args):
22
+ arg = args[i]
23
+ if arg == "-a" or arg == "--all":
24
+ show_all = True
25
+ elif arg == "-s" or arg == "--summarize":
26
+ summary_only = True
27
+ elif arg == "-h" or arg == "--human-readable":
28
+ human_readable = True
29
+ elif arg == "-c" or arg == "--total":
30
+ show_total = True
31
+ elif arg.startswith("--max-depth="):
32
+ try:
33
+ max_depth = int(arg[12:])
34
+ except ValueError:
35
+ return ExecResult(
36
+ stdout="",
37
+ stderr=f"du: invalid maximum depth '{arg[12:]}'\n",
38
+ exit_code=1,
39
+ )
40
+ elif arg == "--help":
41
+ return ExecResult(
42
+ stdout="Usage: du [OPTION]... [FILE]...\n",
43
+ stderr="",
44
+ exit_code=0,
45
+ )
46
+ elif arg.startswith("-"):
47
+ pass # Ignore unknown options
48
+ else:
49
+ paths.append(arg)
50
+ i += 1
51
+
52
+ if not paths:
53
+ paths = ["."]
54
+
55
+ output_lines = []
56
+ total_size = 0
57
+
58
+ for path in paths:
59
+ try:
60
+ resolved = ctx.fs.resolve_path(ctx.cwd, path)
61
+ stat = await ctx.fs.stat(resolved)
62
+
63
+ if stat.is_directory:
64
+ size = await self._get_dir_size(
65
+ ctx, resolved, path, show_all, summary_only,
66
+ max_depth, 0, output_lines, human_readable
67
+ )
68
+ else:
69
+ size = stat.size
70
+ size_str = self._format_size(size, human_readable)
71
+ output_lines.append(f"{size_str}\t{path}")
72
+
73
+ total_size += size
74
+
75
+ except FileNotFoundError:
76
+ return ExecResult(
77
+ stdout="",
78
+ stderr=f"du: cannot access '{path}': No such file or directory\n",
79
+ exit_code=1,
80
+ )
81
+
82
+ if show_total:
83
+ size_str = self._format_size(total_size, human_readable)
84
+ output_lines.append(f"{size_str}\ttotal")
85
+
86
+ return ExecResult(
87
+ stdout="\n".join(output_lines) + "\n" if output_lines else "",
88
+ stderr="",
89
+ exit_code=0,
90
+ )
91
+
92
+ async def _get_dir_size(
93
+ self, ctx: CommandContext, path: str, display_path: str,
94
+ show_all: bool, summary_only: bool, max_depth: int | None,
95
+ current_depth: int, output_lines: list[str], human_readable: bool
96
+ ) -> int:
97
+ """Get directory size recursively."""
98
+ total = 0
99
+
100
+ try:
101
+ entries = await ctx.fs.readdir(path)
102
+
103
+ for entry in entries:
104
+ entry_path = f"{path}/{entry}"
105
+ entry_display = f"{display_path}/{entry}"
106
+
107
+ try:
108
+ stat = await ctx.fs.stat(entry_path)
109
+
110
+ if stat.is_directory:
111
+ size = await self._get_dir_size(
112
+ ctx, entry_path, entry_display, show_all,
113
+ summary_only, max_depth, current_depth + 1,
114
+ output_lines, human_readable
115
+ )
116
+ else:
117
+ size = stat.size
118
+ if show_all and not summary_only:
119
+ if max_depth is None or current_depth < max_depth:
120
+ size_str = self._format_size(size, human_readable)
121
+ output_lines.append(f"{size_str}\t{entry_display}")
122
+
123
+ total += size
124
+ except Exception:
125
+ pass
126
+
127
+ except Exception:
128
+ pass
129
+
130
+ # Output this directory
131
+ if not summary_only or current_depth == 0:
132
+ if max_depth is None or current_depth <= max_depth:
133
+ size_str = self._format_size(total, human_readable)
134
+ output_lines.append(f"{size_str}\t{display_path}")
135
+
136
+ return total
137
+
138
+ def _format_size(self, size: int, human_readable: bool) -> str:
139
+ """Format size for display."""
140
+ if not human_readable:
141
+ return str(size // 1024 or 1) # Return in KB, minimum 1
142
+
143
+ if size < 1024:
144
+ return f"{size}B"
145
+ elif size < 1024 * 1024:
146
+ return f"{size // 1024}K"
147
+ elif size < 1024 * 1024 * 1024:
148
+ return f"{size // (1024 * 1024)}M"
149
+ else:
150
+ return f"{size // (1024 * 1024 * 1024)}G"
@@ -0,0 +1,5 @@
1
+ """Echo command implementation."""
2
+
3
+ from .echo import EchoCommand
4
+
5
+ __all__ = ["EchoCommand"]
@@ -0,0 +1,125 @@
1
+ """Echo command implementation.
2
+
3
+ Usage: echo [-neE] [string ...]
4
+
5
+ Options:
6
+ -n Do not output the trailing newline
7
+ -e Enable interpretation of backslash escapes
8
+ -E Disable interpretation of backslash escapes (default)
9
+ """
10
+
11
+ from ...types import Command, CommandContext, ExecResult
12
+
13
+
14
+ def _process_escapes(s: str) -> str:
15
+ """Process backslash escape sequences in a string."""
16
+ result = []
17
+ i = 0
18
+ while i < len(s):
19
+ if s[i] == "\\" and i + 1 < len(s):
20
+ next_char = s[i + 1]
21
+ if next_char == "n":
22
+ result.append("\n")
23
+ i += 2
24
+ elif next_char == "t":
25
+ result.append("\t")
26
+ i += 2
27
+ elif next_char == "r":
28
+ result.append("\r")
29
+ i += 2
30
+ elif next_char == "\\":
31
+ result.append("\\")
32
+ i += 2
33
+ elif next_char == "a":
34
+ result.append("\a")
35
+ i += 2
36
+ elif next_char == "b":
37
+ result.append("\b")
38
+ i += 2
39
+ elif next_char == "f":
40
+ result.append("\f")
41
+ i += 2
42
+ elif next_char == "v":
43
+ result.append("\v")
44
+ i += 2
45
+ elif next_char == "e":
46
+ result.append("\x1b")
47
+ i += 2
48
+ elif next_char == "0":
49
+ # Octal escape: \0nnn
50
+ octal = ""
51
+ j = i + 2
52
+ while j < len(s) and len(octal) < 3 and s[j] in "01234567":
53
+ octal += s[j]
54
+ j += 1
55
+ if octal:
56
+ result.append(chr(int(octal, 8)))
57
+ else:
58
+ result.append("\0")
59
+ i = j
60
+ elif next_char == "x":
61
+ # Hex escape: \xHH
62
+ hex_digits = ""
63
+ j = i + 2
64
+ while j < len(s) and len(hex_digits) < 2 and s[j] in "0123456789abcdefABCDEF":
65
+ hex_digits += s[j]
66
+ j += 1
67
+ if hex_digits:
68
+ result.append(chr(int(hex_digits, 16)))
69
+ i = j
70
+ else:
71
+ result.append(s[i])
72
+ i += 1
73
+ elif next_char == "c":
74
+ # \c stops output
75
+ return "".join(result)
76
+ else:
77
+ result.append(s[i])
78
+ i += 1
79
+ else:
80
+ result.append(s[i])
81
+ i += 1
82
+ return "".join(result)
83
+
84
+
85
+ class EchoCommand:
86
+ """The echo command."""
87
+
88
+ name = "echo"
89
+
90
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
91
+ """Execute the echo command."""
92
+ newline = True
93
+ enable_escapes = False
94
+
95
+ # Parse options
96
+ i = 0
97
+ while i < len(args):
98
+ arg = args[i]
99
+ if arg.startswith("-") and len(arg) > 1 and all(c in "neE" for c in arg[1:]):
100
+ for c in arg[1:]:
101
+ if c == "n":
102
+ newline = False
103
+ elif c == "e":
104
+ enable_escapes = True
105
+ elif c == "E":
106
+ enable_escapes = False
107
+ i += 1
108
+ else:
109
+ break
110
+
111
+ # Get remaining arguments
112
+ text_args = args[i:]
113
+
114
+ # Build output
115
+ output = " ".join(text_args)
116
+
117
+ # Process escape sequences if enabled
118
+ if enable_escapes:
119
+ output = _process_escapes(output)
120
+
121
+ # Add newline if needed
122
+ if newline:
123
+ output += "\n"
124
+
125
+ return ExecResult(stdout=output, stderr="", exit_code=0)
@@ -0,0 +1,5 @@
1
+ """Env and printenv commands."""
2
+
3
+ from .env import EnvCommand, PrintenvCommand
4
+
5
+ __all__ = ["EnvCommand", "PrintenvCommand"]
@@ -0,0 +1,163 @@
1
+ """Env and printenv command implementations."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class EnvCommand:
7
+ """The env command - run a command with modified environment."""
8
+
9
+ name = "env"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the env command."""
13
+ ignore_environment = False
14
+ unset_vars: list[str] = []
15
+ set_vars: dict[str, str] = {}
16
+ command: list[str] = []
17
+
18
+ i = 0
19
+ while i < len(args):
20
+ arg = args[i]
21
+
22
+ if arg == "--help":
23
+ return ExecResult(
24
+ stdout=(
25
+ "Usage: env [OPTION]... [NAME=VALUE]... [COMMAND [ARG]...]\n"
26
+ "Set each NAME to VALUE in the environment and run COMMAND.\n\n"
27
+ "Options:\n"
28
+ " -i, --ignore-environment start with an empty environment\n"
29
+ " -u, --unset=NAME remove variable from the environment\n"
30
+ " --help display this help and exit\n"
31
+ ),
32
+ stderr="",
33
+ exit_code=0,
34
+ )
35
+ elif arg in ("-i", "--ignore-environment"):
36
+ ignore_environment = True
37
+ elif arg == "-u" and i + 1 < len(args):
38
+ i += 1
39
+ unset_vars.append(args[i])
40
+ elif arg.startswith("-u"):
41
+ unset_vars.append(arg[2:])
42
+ elif arg.startswith("--unset="):
43
+ unset_vars.append(arg[8:])
44
+ elif arg == "--unset" and i + 1 < len(args):
45
+ i += 1
46
+ unset_vars.append(args[i])
47
+ elif arg == "--":
48
+ # Everything after -- is the command
49
+ command = args[i + 1:]
50
+ break
51
+ elif "=" in arg and not arg.startswith("-"):
52
+ # NAME=VALUE assignment
53
+ eq_idx = arg.index("=")
54
+ name = arg[:eq_idx]
55
+ value = arg[eq_idx + 1:]
56
+ set_vars[name] = value
57
+ elif arg.startswith("-"):
58
+ # Unknown option
59
+ return ExecResult(
60
+ stdout="",
61
+ stderr=f"env: invalid option -- '{arg[1:]}'\n",
62
+ exit_code=1,
63
+ )
64
+ else:
65
+ # Start of command
66
+ command = args[i:]
67
+ break
68
+ i += 1
69
+
70
+ # Build the environment
71
+ if ignore_environment:
72
+ new_env = {}
73
+ else:
74
+ new_env = dict(ctx.env)
75
+
76
+ # Remove unset variables
77
+ for var in unset_vars:
78
+ new_env.pop(var, None)
79
+
80
+ # Add new variables
81
+ new_env.update(set_vars)
82
+
83
+ # If no command, print the environment
84
+ if not command:
85
+ lines = [f"{k}={v}" for k, v in sorted(new_env.items())]
86
+ return ExecResult(
87
+ stdout="\n".join(lines) + "\n" if lines else "",
88
+ stderr="",
89
+ exit_code=0,
90
+ )
91
+
92
+ # Execute the command with the new environment
93
+ if not ctx.exec:
94
+ return ExecResult(
95
+ stdout="",
96
+ stderr="env: cannot execute commands\n",
97
+ exit_code=126,
98
+ )
99
+
100
+ # Quote arguments properly for shell execution
101
+ def quote(s: str) -> str:
102
+ if not s or any(c in s for c in " \t\n'\"\\$`!"):
103
+ return "'" + s.replace("'", "'\"'\"'") + "'"
104
+ return s
105
+
106
+ cmd_str = " ".join(quote(c) for c in command)
107
+
108
+ try:
109
+ result = await ctx.exec(cmd_str, {"cwd": ctx.cwd, "env": new_env})
110
+ return result
111
+ except Exception as e:
112
+ return ExecResult(
113
+ stdout="",
114
+ stderr=f"env: {e}\n",
115
+ exit_code=1,
116
+ )
117
+
118
+
119
+ class PrintenvCommand:
120
+ """The printenv command - print environment variables."""
121
+
122
+ name = "printenv"
123
+
124
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
125
+ """Execute the printenv command."""
126
+ var_names: list[str] = []
127
+
128
+ for arg in args:
129
+ if arg == "--help":
130
+ return ExecResult(
131
+ stdout="Usage: printenv [OPTION]... [VARIABLE]...\n",
132
+ stderr="",
133
+ exit_code=0,
134
+ )
135
+ elif arg.startswith("-"):
136
+ pass # Ignore options
137
+ else:
138
+ var_names.append(arg)
139
+
140
+ if not var_names:
141
+ # Print all
142
+ lines = [f"{k}={v}" for k, v in sorted(ctx.env.items())]
143
+ return ExecResult(
144
+ stdout="\n".join(lines) + "\n" if lines else "",
145
+ stderr="",
146
+ exit_code=0,
147
+ )
148
+
149
+ # Print specific variables
150
+ output_lines = []
151
+ exit_code = 0
152
+
153
+ for name in var_names:
154
+ if name in ctx.env:
155
+ output_lines.append(ctx.env[name])
156
+ else:
157
+ exit_code = 1
158
+
159
+ output = "\n".join(output_lines)
160
+ if output:
161
+ output += "\n"
162
+
163
+ return ExecResult(stdout=output, stderr="", exit_code=exit_code)
@@ -0,0 +1,5 @@
1
+ """Expand and unexpand commands."""
2
+
3
+ from .expand import ExpandCommand, UnexpandCommand
4
+
5
+ __all__ = ["ExpandCommand", "UnexpandCommand"]