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,56 @@
1
+ """Export builtin implementation.
2
+
3
+ Usage: export [name[=value] ...]
4
+
5
+ Mark variables for export to child processes. If no arguments are given,
6
+ list all exported variables.
7
+ """
8
+
9
+ import re
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from ..types import InterpreterContext
14
+ from ...types import ExecResult
15
+
16
+
17
+ async def handle_export(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
18
+ """Execute the export builtin."""
19
+ from ...types import ExecResult
20
+
21
+ # No arguments: list all exported variables
22
+ if not args:
23
+ lines = []
24
+ for k, v in sorted(ctx.state.env.items()):
25
+ # Skip internal variables
26
+ if k.startswith("PIPESTATUS_") or k == "?" or k == "#":
27
+ continue
28
+ # Escape special characters in value
29
+ escaped_v = v.replace("\\", "\\\\").replace('"', '\\"')
30
+ lines.append(f'declare -x {k}="{escaped_v}"')
31
+ return ExecResult(stdout="\n".join(lines) + "\n" if lines else "", stderr="", exit_code=0)
32
+
33
+ # Process each argument
34
+ for arg in args:
35
+ # Skip options
36
+ if arg.startswith("-"):
37
+ continue
38
+
39
+ if "=" in arg:
40
+ name, value = arg.split("=", 1)
41
+ else:
42
+ # Export existing variable or create empty
43
+ name = arg
44
+ value = ctx.state.env.get(arg, "")
45
+
46
+ # Validate identifier
47
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
48
+ return ExecResult(
49
+ stdout="",
50
+ stderr=f"bash: export: '{name}': not a valid identifier\n",
51
+ exit_code=1,
52
+ )
53
+
54
+ ctx.state.env[name] = value
55
+
56
+ return ExecResult(stdout="", stderr="", exit_code=0)
@@ -0,0 +1,44 @@
1
+ """Let builtin implementation.
2
+
3
+ Usage: let expr [expr ...]
4
+
5
+ Evaluates arithmetic expressions. Each expr is evaluated as an arithmetic
6
+ expression. Returns 0 if the last expression evaluates to non-zero, 1 otherwise.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from ..types import InterpreterContext
13
+ from ...types import ExecResult
14
+
15
+
16
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
17
+ """Create an ExecResult."""
18
+ from ...types import ExecResult
19
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
20
+
21
+
22
+ async def handle_let(
23
+ ctx: "InterpreterContext", args: list[str]
24
+ ) -> "ExecResult":
25
+ """Execute the let builtin."""
26
+ from ..expansion import evaluate_arithmetic_sync
27
+ from ...parser.parser import Parser
28
+
29
+ if not args:
30
+ return _result("", "bash: let: expression expected\n", 1)
31
+
32
+ parser = Parser()
33
+ last_value = 0
34
+
35
+ for expr_str in args:
36
+ try:
37
+ # Parse and evaluate the arithmetic expression
38
+ arith_expr = parser._parse_arithmetic_expression(expr_str)
39
+ last_value = evaluate_arithmetic_sync(ctx, arith_expr)
40
+ except Exception as e:
41
+ return _result("", f"bash: let: {expr_str}: syntax error\n", 1)
42
+
43
+ # Return 0 if last value is non-zero, 1 if zero
44
+ return _result("", "", 0 if last_value != 0 else 1)
@@ -0,0 +1,57 @@
1
+ """Local builtin implementation.
2
+
3
+ Usage: local [name[=value] ...]
4
+
5
+ Create local variables for use within a function. When the function
6
+ returns, any local variables are restored to their previous values.
7
+ """
8
+
9
+ import re
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from ..types import InterpreterContext
14
+ from ...types import ExecResult
15
+
16
+
17
+ async def handle_local(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
18
+ """Execute the local builtin."""
19
+ from ...types import ExecResult
20
+
21
+ # Check if we're inside a function
22
+ if not ctx.state.local_scopes:
23
+ return ExecResult(
24
+ stdout="",
25
+ stderr="bash: local: can only be used in a function\n",
26
+ exit_code=1,
27
+ )
28
+
29
+ current_scope = ctx.state.local_scopes[-1]
30
+
31
+ for arg in args:
32
+ # Skip options
33
+ if arg.startswith("-"):
34
+ continue
35
+
36
+ if "=" in arg:
37
+ name, value = arg.split("=", 1)
38
+ else:
39
+ name = arg
40
+ value = ""
41
+
42
+ # Validate identifier
43
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
44
+ return ExecResult(
45
+ stdout="",
46
+ stderr=f"bash: local: '{name}': not a valid identifier\n",
47
+ exit_code=1,
48
+ )
49
+
50
+ # Save original value for restoration (if not already saved)
51
+ if name not in current_scope:
52
+ current_scope[name] = ctx.state.env.get(name)
53
+
54
+ # Set the new value
55
+ ctx.state.env[name] = value
56
+
57
+ return ExecResult(stdout="", stderr="", exit_code=0)
@@ -0,0 +1,152 @@
1
+ """Mapfile/readarray builtin implementation.
2
+
3
+ Usage: mapfile [-d delim] [-n count] [-O origin] [-s count] [-t] [array]
4
+ readarray [-d delim] [-n count] [-O origin] [-s count] [-t] [array]
5
+
6
+ Reads lines from stdin into an array variable.
7
+
8
+ Options:
9
+ -d delim Use delim as line delimiter instead of newline
10
+ -n count Read at most count lines (0 means all)
11
+ -O origin Begin assigning at index origin (default 0)
12
+ -s count Skip the first count lines
13
+ -t Remove trailing delimiter from each line
14
+ array Name of array variable (default: MAPFILE)
15
+ """
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from ..types import InterpreterContext
21
+ from ...types import ExecResult
22
+
23
+
24
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
25
+ """Create an ExecResult."""
26
+ from ...types import ExecResult
27
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
28
+
29
+
30
+ async def handle_mapfile(
31
+ ctx: "InterpreterContext", args: list[str], stdin: str = ""
32
+ ) -> "ExecResult":
33
+ """Execute the mapfile/readarray builtin."""
34
+ # Parse options
35
+ delimiter = "\n"
36
+ max_count = 0 # 0 means unlimited
37
+ origin = 0
38
+ skip_count = 0
39
+ strip_trailing = False
40
+ array_name = "MAPFILE"
41
+
42
+ i = 0
43
+ while i < len(args):
44
+ arg = args[i]
45
+
46
+ if arg == "--":
47
+ if i + 1 < len(args):
48
+ array_name = args[i + 1]
49
+ break
50
+
51
+ if arg == "-d" and i + 1 < len(args):
52
+ delimiter = args[i + 1]
53
+ # Handle escape sequences
54
+ if delimiter == "\\n":
55
+ delimiter = "\n"
56
+ elif delimiter == "\\t":
57
+ delimiter = "\t"
58
+ elif delimiter == "":
59
+ delimiter = "\0" # NUL delimiter
60
+ i += 2
61
+ continue
62
+
63
+ if arg == "-n" and i + 1 < len(args):
64
+ try:
65
+ max_count = int(args[i + 1])
66
+ except ValueError:
67
+ return _result("", f"bash: mapfile: {args[i + 1]}: invalid count\n", 1)
68
+ i += 2
69
+ continue
70
+
71
+ if arg == "-O" and i + 1 < len(args):
72
+ try:
73
+ origin = int(args[i + 1])
74
+ except ValueError:
75
+ return _result("", f"bash: mapfile: {args[i + 1]}: invalid origin\n", 1)
76
+ i += 2
77
+ continue
78
+
79
+ if arg == "-s" and i + 1 < len(args):
80
+ try:
81
+ skip_count = int(args[i + 1])
82
+ except ValueError:
83
+ return _result("", f"bash: mapfile: {args[i + 1]}: invalid count\n", 1)
84
+ i += 2
85
+ continue
86
+
87
+ if arg == "-t":
88
+ strip_trailing = True
89
+ i += 1
90
+ continue
91
+
92
+ if arg.startswith("-"):
93
+ # Unknown option - might be combined like -tn
94
+ for c in arg[1:]:
95
+ if c == "t":
96
+ strip_trailing = True
97
+ elif c == "d":
98
+ return _result("", "bash: mapfile: -d: option requires an argument\n", 1)
99
+ elif c == "n":
100
+ return _result("", "bash: mapfile: -n: option requires an argument\n", 1)
101
+ elif c == "O":
102
+ return _result("", "bash: mapfile: -O: option requires an argument\n", 1)
103
+ elif c == "s":
104
+ return _result("", "bash: mapfile: -s: option requires an argument\n", 1)
105
+ else:
106
+ return _result("", f"bash: mapfile: -{c}: invalid option\n", 2)
107
+ i += 1
108
+ continue
109
+
110
+ # Not an option - must be array name
111
+ array_name = arg
112
+ i += 1
113
+
114
+ # Clear existing array elements
115
+ prefix = f"{array_name}_"
116
+ to_remove = [k for k in ctx.state.env if k.startswith(prefix) and not k.startswith(f"{array_name}__")]
117
+ for k in to_remove:
118
+ del ctx.state.env[k]
119
+
120
+ # Mark as array
121
+ ctx.state.env[f"{array_name}__is_array"] = "indexed"
122
+
123
+ # Split input by delimiter
124
+ if not stdin:
125
+ return _result("", "", 0)
126
+
127
+ if delimiter == "\n":
128
+ lines = stdin.split("\n")
129
+ # Remove last empty element if input ends with newline
130
+ if lines and lines[-1] == "":
131
+ lines = lines[:-1]
132
+ else:
133
+ lines = stdin.split(delimiter)
134
+
135
+ # Skip first N lines
136
+ if skip_count > 0:
137
+ lines = lines[skip_count:]
138
+
139
+ # Limit to N lines
140
+ if max_count > 0:
141
+ lines = lines[:max_count]
142
+
143
+ # Store lines in array
144
+ for idx, line in enumerate(lines):
145
+ if strip_trailing:
146
+ # Remove trailing delimiter (already split, so just strip the char if present)
147
+ if delimiter != "\n" and line.endswith(delimiter):
148
+ line = line[:-len(delimiter)]
149
+
150
+ ctx.state.env[f"{array_name}_{origin + idx}"] = line
151
+
152
+ return _result("", "", 0)
@@ -0,0 +1,378 @@
1
+ """Miscellaneous builtins: colon, true, false, type, command, builtin, exec, wait.
2
+
3
+ These are simple builtins that don't need their own files.
4
+ """
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..types import InterpreterContext
10
+ from ...types import ExecResult
11
+
12
+
13
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
14
+ """Create an ExecResult."""
15
+ from ...types import ExecResult
16
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
17
+
18
+
19
+ async def handle_colon(
20
+ ctx: "InterpreterContext", args: list[str]
21
+ ) -> "ExecResult":
22
+ """Execute the : (colon) builtin - null command, always succeeds."""
23
+ return _result("", "", 0)
24
+
25
+
26
+ async def handle_true(
27
+ ctx: "InterpreterContext", args: list[str]
28
+ ) -> "ExecResult":
29
+ """Execute the true builtin - always succeeds."""
30
+ return _result("", "", 0)
31
+
32
+
33
+ async def handle_false(
34
+ ctx: "InterpreterContext", args: list[str]
35
+ ) -> "ExecResult":
36
+ """Execute the false builtin - always fails."""
37
+ return _result("", "", 1)
38
+
39
+
40
+ async def handle_type(
41
+ ctx: "InterpreterContext", args: list[str]
42
+ ) -> "ExecResult":
43
+ """Execute the type builtin - display information about command type.
44
+
45
+ Usage: type [-afptP] name [name ...]
46
+
47
+ Options:
48
+ -a Display all locations containing an executable named name
49
+ -f Suppress shell function lookup
50
+ -p Display path to executable (like which)
51
+ -P Force path search even for builtins
52
+ -t Output a single word: alias, keyword, function, builtin, file, or ''
53
+ """
54
+ from .alias import get_aliases
55
+
56
+ # Parse options
57
+ show_all = False
58
+ no_functions = False
59
+ path_only = False
60
+ force_path = False
61
+ type_only = False
62
+ names = []
63
+
64
+ i = 0
65
+ while i < len(args):
66
+ arg = args[i]
67
+ if arg == "--":
68
+ names.extend(args[i + 1:])
69
+ break
70
+ elif arg.startswith("-") and len(arg) > 1:
71
+ for c in arg[1:]:
72
+ if c == "a":
73
+ show_all = True
74
+ elif c == "f":
75
+ no_functions = True
76
+ elif c == "p":
77
+ path_only = True
78
+ elif c == "P":
79
+ force_path = True
80
+ elif c == "t":
81
+ type_only = True
82
+ else:
83
+ return _result("", f"bash: type: -{c}: invalid option\n", 1)
84
+ else:
85
+ names.append(arg)
86
+ i += 1
87
+
88
+ if not names:
89
+ return _result("", "bash: type: usage: type [-afptP] name [name ...]\n", 1)
90
+
91
+ # Keywords
92
+ keywords = {
93
+ "if", "then", "else", "elif", "fi", "case", "esac", "for", "select",
94
+ "while", "until", "do", "done", "in", "function", "time", "coproc",
95
+ "{", "}", "!", "[[", "]]"
96
+ }
97
+
98
+ # Get builtins
99
+ from . import BUILTINS
100
+
101
+ # Get aliases
102
+ aliases = get_aliases(ctx)
103
+
104
+ # Get functions
105
+ functions = getattr(ctx.state, 'functions', {})
106
+
107
+ output = []
108
+ exit_code = 0
109
+
110
+ for name in names:
111
+ found = False
112
+
113
+ # Check alias (unless -f)
114
+ if not no_functions and name in aliases:
115
+ found = True
116
+ if type_only:
117
+ output.append("alias")
118
+ elif path_only:
119
+ pass # -p doesn't show aliases
120
+ else:
121
+ output.append(f"{name} is aliased to `{aliases[name]}'")
122
+ if not show_all:
123
+ continue
124
+
125
+ # Check keyword
126
+ if name in keywords:
127
+ found = True
128
+ if type_only:
129
+ output.append("keyword")
130
+ elif not path_only:
131
+ output.append(f"{name} is a shell keyword")
132
+ if not show_all:
133
+ continue
134
+
135
+ # Check function (unless -f or -P)
136
+ if not no_functions and not force_path and name in functions:
137
+ found = True
138
+ if type_only:
139
+ output.append("function")
140
+ elif not path_only:
141
+ output.append(f"{name} is a function")
142
+ if not show_all:
143
+ continue
144
+
145
+ # Check builtin (unless -P)
146
+ if not force_path and name in BUILTINS:
147
+ found = True
148
+ if type_only:
149
+ output.append("builtin")
150
+ elif not path_only:
151
+ output.append(f"{name} is a shell builtin")
152
+ if not show_all:
153
+ continue
154
+
155
+ # Check command registry
156
+ from ...commands import COMMAND_NAMES
157
+ if name in COMMAND_NAMES:
158
+ found = True
159
+ if type_only:
160
+ output.append("file")
161
+ elif path_only:
162
+ output.append(name)
163
+ else:
164
+ output.append(f"{name} is {name}")
165
+ if not show_all:
166
+ continue
167
+
168
+ if not found:
169
+ if type_only:
170
+ output.append("")
171
+ else:
172
+ output.append(f"bash: type: {name}: not found")
173
+ exit_code = 1
174
+
175
+ if output:
176
+ return _result("\n".join(output) + "\n", "", exit_code)
177
+ return _result("", "", exit_code)
178
+
179
+
180
+ async def handle_command(
181
+ ctx: "InterpreterContext", args: list[str]
182
+ ) -> "ExecResult":
183
+ """Execute the command builtin - run command bypassing functions.
184
+
185
+ Usage: command [-pVv] command [arguments ...]
186
+
187
+ Options:
188
+ -p Use a default path to search for command
189
+ -v Display description of command (like type)
190
+ -V Display verbose description of command
191
+ """
192
+ # Parse options
193
+ describe = False
194
+ verbose = False
195
+ use_default_path = False
196
+ cmd_args = []
197
+
198
+ i = 0
199
+ while i < len(args):
200
+ arg = args[i]
201
+ if arg == "--":
202
+ cmd_args = args[i + 1:]
203
+ break
204
+ elif arg.startswith("-") and len(arg) > 1 and not cmd_args:
205
+ for c in arg[1:]:
206
+ if c == "p":
207
+ use_default_path = True
208
+ elif c == "v":
209
+ describe = True
210
+ elif c == "V":
211
+ verbose = True
212
+ else:
213
+ return _result("", f"bash: command: -{c}: invalid option\n", 1)
214
+ else:
215
+ cmd_args = args[i:]
216
+ break
217
+ i += 1
218
+
219
+ if not cmd_args:
220
+ if describe or verbose:
221
+ return _result("", "", 0)
222
+ return _result("", "", 0)
223
+
224
+ cmd_name = cmd_args[0]
225
+
226
+ # Handle -v or -V: describe the command
227
+ if describe or verbose:
228
+ from . import BUILTINS
229
+ from ...commands import COMMAND_NAMES
230
+
231
+ if cmd_name in BUILTINS:
232
+ if verbose:
233
+ return _result(f"{cmd_name} is a shell builtin\n", "", 0)
234
+ else:
235
+ return _result(f"{cmd_name}\n", "", 0)
236
+ elif cmd_name in COMMAND_NAMES:
237
+ if verbose:
238
+ return _result(f"{cmd_name} is {cmd_name}\n", "", 0)
239
+ else:
240
+ return _result(f"{cmd_name}\n", "", 0)
241
+ else:
242
+ return _result("", f"bash: command: {cmd_name}: not found\n", 1)
243
+
244
+ # Execute the command, bypassing functions
245
+ # Store current function state and temporarily hide the function
246
+ functions = getattr(ctx.state, 'functions', {})
247
+ hidden_func = functions.pop(cmd_name, None)
248
+
249
+ try:
250
+ # Build command string with proper quoting
251
+ def shell_quote(s: str) -> str:
252
+ if not s or any(c in s for c in ' \t\n\'"\\$`!'):
253
+ return "'" + s.replace("'", "'\\''") + "'"
254
+ return s
255
+
256
+ cmd_str = " ".join(shell_quote(a) for a in cmd_args)
257
+ result = await ctx.exec_fn(cmd_str, None, None)
258
+ return result
259
+ finally:
260
+ # Restore function if it was hidden
261
+ if hidden_func is not None:
262
+ functions[cmd_name] = hidden_func
263
+
264
+
265
+ async def handle_builtin(
266
+ ctx: "InterpreterContext", args: list[str]
267
+ ) -> "ExecResult":
268
+ """Execute the builtin builtin - run shell builtin directly.
269
+
270
+ Usage: builtin [shell-builtin [args]]
271
+ """
272
+ if not args:
273
+ return _result("", "", 0)
274
+
275
+ builtin_name = args[0]
276
+ builtin_args = args[1:]
277
+
278
+ from . import BUILTINS
279
+
280
+ if builtin_name not in BUILTINS:
281
+ return _result("", f"bash: builtin: {builtin_name}: not a shell builtin\n", 1)
282
+
283
+ handler = BUILTINS[builtin_name]
284
+ return await handler(ctx, builtin_args)
285
+
286
+
287
+ async def handle_exec(
288
+ ctx: "InterpreterContext", args: list[str]
289
+ ) -> "ExecResult":
290
+ """Execute the exec builtin - replace shell with command.
291
+
292
+ Usage: exec [-cl] [-a name] [command [arguments ...]]
293
+
294
+ In a sandboxed environment, this just executes the command normally
295
+ since we can't actually replace the process.
296
+
297
+ Options:
298
+ -c Execute command with empty environment
299
+ -l Pass dash as zeroth argument (login shell)
300
+ -a name Pass name as zeroth argument
301
+ """
302
+ # Parse options
303
+ clear_env = False
304
+ login_shell = False
305
+ arg0_name = None
306
+ cmd_args = []
307
+
308
+ i = 0
309
+ while i < len(args):
310
+ arg = args[i]
311
+ if arg == "--":
312
+ cmd_args = args[i + 1:]
313
+ break
314
+ elif arg == "-c" and not cmd_args:
315
+ clear_env = True
316
+ elif arg == "-l" and not cmd_args:
317
+ login_shell = True
318
+ elif arg == "-a" and not cmd_args and i + 1 < len(args):
319
+ i += 1
320
+ arg0_name = args[i]
321
+ elif arg.startswith("-") and not cmd_args:
322
+ # Combined options
323
+ for c in arg[1:]:
324
+ if c == "c":
325
+ clear_env = True
326
+ elif c == "l":
327
+ login_shell = True
328
+ else:
329
+ return _result("", f"bash: exec: -{c}: invalid option\n", 1)
330
+ else:
331
+ cmd_args = args[i:]
332
+ break
333
+ i += 1
334
+
335
+ # If no command, exec just affects redirections (which we don't handle here)
336
+ if not cmd_args:
337
+ return _result("", "", 0)
338
+
339
+ # In sandboxed mode, just execute the command
340
+ def shell_quote(s: str) -> str:
341
+ if not s or any(c in s for c in ' \t\n\'"\\$`!'):
342
+ return "'" + s.replace("'", "'\\''") + "'"
343
+ return s
344
+
345
+ cmd_str = " ".join(shell_quote(a) for a in cmd_args)
346
+ result = await ctx.exec_fn(cmd_str, None, None)
347
+ return result
348
+
349
+
350
+ async def handle_wait(
351
+ ctx: "InterpreterContext", args: list[str]
352
+ ) -> "ExecResult":
353
+ """Execute the wait builtin - wait for background jobs.
354
+
355
+ Usage: wait [-fn] [-p var] [id ...]
356
+
357
+ In a sandboxed environment without true background jobs,
358
+ this is mostly a no-op but returns success.
359
+
360
+ Options:
361
+ -f Wait for job termination (not just state change)
362
+ -n Wait for any job to complete
363
+ -p var Store PID in var
364
+ """
365
+ # Parse options
366
+ i = 0
367
+ while i < len(args):
368
+ arg = args[i]
369
+ if arg == "--":
370
+ break
371
+ elif arg.startswith("-"):
372
+ # Accept but ignore options since we don't have real job control
373
+ if arg == "-p" and i + 1 < len(args):
374
+ i += 1
375
+ i += 1
376
+
377
+ # No real job control in sandboxed environment, return success
378
+ return _result("", "", 0)