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,80 @@
1
+ """Readonly builtin implementation.
2
+
3
+ Usage: readonly [-p] [name[=value] ...]
4
+
5
+ Marks variables as readonly. Once a variable is marked readonly, it cannot
6
+ be reassigned or unset.
7
+
8
+ Options:
9
+ -p Display all readonly variables
10
+ """
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from ..types import InterpreterContext
16
+ from ...types import ExecResult
17
+
18
+
19
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
20
+ """Create an ExecResult."""
21
+ from ...types import ExecResult
22
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
23
+
24
+
25
+ async def handle_readonly(
26
+ ctx: "InterpreterContext", args: list[str]
27
+ ) -> "ExecResult":
28
+ """Execute the readonly builtin."""
29
+ # Parse options
30
+ show_all = False
31
+ names = []
32
+
33
+ i = 0
34
+ while i < len(args):
35
+ arg = args[i]
36
+ if arg == "-p":
37
+ show_all = True
38
+ elif arg == "--":
39
+ names.extend(args[i + 1:])
40
+ break
41
+ elif arg.startswith("-"):
42
+ # Unknown option - ignore for now
43
+ pass
44
+ else:
45
+ names.append(arg)
46
+ i += 1
47
+
48
+ # If no names and -p or no args, show all readonly variables
49
+ if not names or show_all:
50
+ output = []
51
+ readonly_vars = ctx.state.env.get("__readonly__", "").split()
52
+ for var in sorted(readonly_vars):
53
+ if var in ctx.state.env:
54
+ value = ctx.state.env[var]
55
+ output.append(f"declare -r {var}=\"{value}\"")
56
+ else:
57
+ output.append(f"declare -r {var}")
58
+ if output:
59
+ return _result("\n".join(output) + "\n", "", 0)
60
+ return _result("", "", 0)
61
+
62
+ # Mark variables as readonly
63
+ readonly_set = set(ctx.state.env.get("__readonly__", "").split())
64
+
65
+ for name_value in names:
66
+ if "=" in name_value:
67
+ name, value = name_value.split("=", 1)
68
+ # Check if already readonly
69
+ if name in readonly_set:
70
+ return _result("", f"bash: readonly: {name}: readonly variable\n", 1)
71
+ ctx.state.env[name] = value
72
+ else:
73
+ name = name_value
74
+
75
+ readonly_set.add(name)
76
+
77
+ # Store readonly set
78
+ ctx.state.env["__readonly__"] = " ".join(sorted(readonly_set))
79
+
80
+ return _result("", "", 0)
@@ -0,0 +1,234 @@
1
+ """Set and shift builtin implementations.
2
+
3
+ set - Set or unset shell options and positional parameters.
4
+
5
+ Usage: set [options] [-- arg ...]
6
+ set +o
7
+ set -o [option]
8
+
9
+ Options:
10
+ -e errexit Exit immediately if a command exits with non-zero status
11
+ -u nounset Treat unset variables as an error when substituting
12
+ -x xtrace Print commands and their arguments as they are executed
13
+ -v verbose Print shell input lines as they are read
14
+ -o pipefail Return exit status of last failing command in pipeline
15
+
16
+ shift - Shift positional parameters.
17
+
18
+ Usage: shift [n]
19
+
20
+ Shift positional parameters to the left by n (default 1).
21
+ """
22
+
23
+ from typing import TYPE_CHECKING
24
+
25
+ if TYPE_CHECKING:
26
+ from ..types import InterpreterContext
27
+ from ...types import ExecResult
28
+
29
+
30
+ async def handle_set(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
31
+ """Execute the set builtin."""
32
+ from ...types import ExecResult
33
+
34
+ # No arguments: print all variables
35
+ if not args:
36
+ lines = []
37
+ for k, v in sorted(ctx.state.env.items()):
38
+ # Skip internal variables
39
+ if k.startswith("PIPESTATUS_") or k == "?":
40
+ continue
41
+ lines.append(f"{k}='{v}'")
42
+ return ExecResult(stdout="\n".join(lines) + "\n", stderr="", exit_code=0)
43
+
44
+ i = 0
45
+ while i < len(args):
46
+ arg = args[i]
47
+
48
+ # Handle -- which starts positional parameters
49
+ if arg == "--":
50
+ # Set positional parameters from remaining args
51
+ new_params = args[i + 1:]
52
+ _set_positional_params(ctx, new_params)
53
+ return ExecResult(stdout="", stderr="", exit_code=0)
54
+
55
+ # Handle -o option
56
+ if arg == "-o":
57
+ if i + 1 < len(args):
58
+ i += 1
59
+ opt_name = args[i]
60
+ result = _set_option(ctx, opt_name, True)
61
+ if result:
62
+ return result
63
+ else:
64
+ # List all options
65
+ stdout = _list_options(ctx)
66
+ return ExecResult(stdout=stdout, stderr="", exit_code=0)
67
+
68
+ # Handle +o option
69
+ elif arg == "+o":
70
+ if i + 1 < len(args):
71
+ i += 1
72
+ opt_name = args[i]
73
+ result = _set_option(ctx, opt_name, False)
74
+ if result:
75
+ return result
76
+ else:
77
+ # List all options in a format that can be re-input
78
+ stdout = _list_options_script(ctx)
79
+ return ExecResult(stdout=stdout, stderr="", exit_code=0)
80
+
81
+ # Handle short options like -e, -u, -x, -v
82
+ elif arg.startswith("-") and len(arg) > 1 and arg[1] != "-":
83
+ for c in arg[1:]:
84
+ result = _set_short_option(ctx, c, True)
85
+ if result:
86
+ return result
87
+
88
+ elif arg.startswith("+") and len(arg) > 1:
89
+ for c in arg[1:]:
90
+ result = _set_short_option(ctx, c, False)
91
+ if result:
92
+ return result
93
+
94
+ # Treat as positional parameter
95
+ else:
96
+ new_params = args[i:]
97
+ _set_positional_params(ctx, new_params)
98
+ return ExecResult(stdout="", stderr="", exit_code=0)
99
+
100
+ i += 1
101
+
102
+ return ExecResult(stdout="", stderr="", exit_code=0)
103
+
104
+
105
+ def _set_positional_params(ctx: "InterpreterContext", params: list[str]) -> None:
106
+ """Set positional parameters $1, $2, etc."""
107
+ # Clear existing positional parameters
108
+ i = 1
109
+ while str(i) in ctx.state.env:
110
+ del ctx.state.env[str(i)]
111
+ i += 1
112
+
113
+ # Set new positional parameters
114
+ for i, param in enumerate(params, start=1):
115
+ ctx.state.env[str(i)] = param
116
+
117
+ # Update $# (number of positional parameters)
118
+ ctx.state.env["#"] = str(len(params))
119
+
120
+
121
+ def _set_option(ctx: "InterpreterContext", name: str, enable: bool) -> "ExecResult | None":
122
+ """Set a named option. Returns error result if invalid option."""
123
+ from ...types import ExecResult
124
+
125
+ options = ctx.state.options
126
+ if name == "errexit":
127
+ options.errexit = enable
128
+ elif name == "nounset":
129
+ options.nounset = enable
130
+ elif name == "xtrace":
131
+ options.xtrace = enable
132
+ elif name == "verbose":
133
+ options.verbose = enable
134
+ elif name == "pipefail":
135
+ options.pipefail = enable
136
+ else:
137
+ return ExecResult(
138
+ stdout="",
139
+ stderr=f"bash: set: {name}: invalid option name\n",
140
+ exit_code=1,
141
+ )
142
+ return None
143
+
144
+
145
+ def _set_short_option(ctx: "InterpreterContext", char: str, enable: bool) -> "ExecResult | None":
146
+ """Set a short option like -e. Returns error result if invalid."""
147
+ from ...types import ExecResult
148
+
149
+ options = ctx.state.options
150
+ if char == "e":
151
+ options.errexit = enable
152
+ elif char == "u":
153
+ options.nounset = enable
154
+ elif char == "x":
155
+ options.xtrace = enable
156
+ elif char == "v":
157
+ options.verbose = enable
158
+ else:
159
+ return ExecResult(
160
+ stdout="",
161
+ stderr=f"bash: set: -{char}: invalid option\n",
162
+ exit_code=1,
163
+ )
164
+ return None
165
+
166
+
167
+ def _list_options(ctx: "InterpreterContext") -> str:
168
+ """List all options in human-readable format."""
169
+ options = ctx.state.options
170
+ lines = [
171
+ f"errexit {'on' if options.errexit else 'off'}",
172
+ f"nounset {'on' if options.nounset else 'off'}",
173
+ f"pipefail {'on' if options.pipefail else 'off'}",
174
+ f"verbose {'on' if options.verbose else 'off'}",
175
+ f"xtrace {'on' if options.xtrace else 'off'}",
176
+ ]
177
+ return "\n".join(lines) + "\n"
178
+
179
+
180
+ def _list_options_script(ctx: "InterpreterContext") -> str:
181
+ """List options in re-inputable script format."""
182
+ options = ctx.state.options
183
+ lines = [
184
+ f"set {'-' if options.errexit else '+'}o errexit",
185
+ f"set {'-' if options.nounset else '+'}o nounset",
186
+ f"set {'-' if options.pipefail else '+'}o pipefail",
187
+ f"set {'-' if options.verbose else '+'}o verbose",
188
+ f"set {'-' if options.xtrace else '+'}o xtrace",
189
+ ]
190
+ return "\n".join(lines) + "\n"
191
+
192
+
193
+ async def handle_shift(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
194
+ """Execute the shift builtin."""
195
+ from ...types import ExecResult
196
+
197
+ # Default shift count is 1
198
+ n = 1
199
+ if args:
200
+ try:
201
+ n = int(args[0])
202
+ except ValueError:
203
+ return ExecResult(
204
+ stdout="",
205
+ stderr=f"bash: shift: {args[0]}: numeric argument required\n",
206
+ exit_code=1,
207
+ )
208
+
209
+ if n < 0:
210
+ return ExecResult(
211
+ stdout="",
212
+ stderr=f"bash: shift: {n}: shift count out of range\n",
213
+ exit_code=1,
214
+ )
215
+
216
+ # Get current positional parameters
217
+ param_count = int(ctx.state.env.get("#", "0"))
218
+
219
+ if n > param_count:
220
+ return ExecResult(
221
+ stdout="",
222
+ stderr=f"bash: shift: {n}: shift count out of range\n",
223
+ exit_code=1,
224
+ )
225
+
226
+ # Collect remaining parameters
227
+ new_params = []
228
+ for i in range(n + 1, param_count + 1):
229
+ new_params.append(ctx.state.env.get(str(i), ""))
230
+
231
+ # Set new positional parameters
232
+ _set_positional_params(ctx, new_params)
233
+
234
+ return ExecResult(stdout="", stderr="", exit_code=0)
@@ -0,0 +1,201 @@
1
+ """Shopt builtin implementation.
2
+
3
+ Usage: shopt [-pqsu] [optname ...]
4
+
5
+ Shell options control various behaviors of the shell.
6
+
7
+ Options:
8
+ -s Enable (set) each optname
9
+ -u Disable (unset) each optname
10
+ -q Quiet mode, suppress output (exit status indicates if option is set)
11
+ -p Print options in a form that can be reused as input
12
+ """
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from ..types import InterpreterContext
18
+ from ...types import ExecResult
19
+
20
+
21
+ # Default states for shell options
22
+ DEFAULT_SHOPTS = {
23
+ # Bash common options
24
+ "expand_aliases": True,
25
+ "extglob": False,
26
+ "extquote": True,
27
+ "failglob": False,
28
+ "globstar": False,
29
+ "nocasematch": False,
30
+ "nullglob": False,
31
+ "dotglob": False,
32
+ "lastpipe": False,
33
+ "xpg_echo": False,
34
+ "progcomp": True,
35
+ "histappend": False,
36
+ "extdebug": False,
37
+ "inherit_errexit": False,
38
+ "command_sub_errexit": False,
39
+ "process_sub_fail": False,
40
+ # Strict options (for oil/ysh compatibility)
41
+ "strict_array": False,
42
+ "strict_arith": False,
43
+ "strict_argv": False,
44
+ "strict_arg_parse": False,
45
+ "strict_control_flow": False,
46
+ "strict_nameref": False,
47
+ "strict_word_eval": False,
48
+ "strict_tilde": False,
49
+ "strict_status": False,
50
+ "strict_binding": False,
51
+ "strict": False,
52
+ # Other options
53
+ "eval_unsafe_arith": False,
54
+ "ysh": False,
55
+ "compat_array": False,
56
+ "nounset": False,
57
+ "parse_at": False,
58
+ "simple_eval_builtin": False,
59
+ "no_last_fork": False,
60
+ "no_fork_last": False,
61
+ "no_dash_glob": False,
62
+ "globskipdots": True,
63
+ "ignore_shopt_not_impl": False,
64
+ "ignore_flags_not_impl": False,
65
+ }
66
+
67
+
68
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
69
+ """Create an ExecResult."""
70
+ from ...types import ExecResult
71
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
72
+
73
+
74
+ def _get_shopts(ctx: "InterpreterContext") -> dict[str, bool]:
75
+ """Get the shell options dictionary from context."""
76
+ # Store shopts in env with __shopt__ prefix
77
+ shopts = {}
78
+ for name, default in DEFAULT_SHOPTS.items():
79
+ key = f"__shopt_{name}__"
80
+ if key in ctx.state.env:
81
+ shopts[name] = ctx.state.env[key] == "1"
82
+ else:
83
+ shopts[name] = default
84
+ return shopts
85
+
86
+
87
+ def _set_shopt(ctx: "InterpreterContext", name: str, value: bool) -> None:
88
+ """Set a shell option."""
89
+ ctx.state.env[f"__shopt_{name}__"] = "1" if value else "0"
90
+
91
+
92
+ async def handle_shopt(
93
+ ctx: "InterpreterContext", args: list[str]
94
+ ) -> "ExecResult":
95
+ """Execute the shopt builtin."""
96
+ # Parse options
97
+ set_mode = False
98
+ unset_mode = False
99
+ quiet_mode = False
100
+ print_mode = False
101
+ optnames = []
102
+
103
+ i = 0
104
+ while i < len(args):
105
+ arg = args[i]
106
+ if arg == "-s":
107
+ set_mode = True
108
+ elif arg == "-u":
109
+ unset_mode = True
110
+ elif arg == "-q":
111
+ quiet_mode = True
112
+ elif arg == "-p":
113
+ print_mode = True
114
+ elif arg == "--":
115
+ optnames.extend(args[i + 1:])
116
+ break
117
+ elif arg.startswith("-"):
118
+ # Handle combined flags like -su
119
+ for c in arg[1:]:
120
+ if c == "s":
121
+ set_mode = True
122
+ elif c == "u":
123
+ unset_mode = True
124
+ elif c == "q":
125
+ quiet_mode = True
126
+ elif c == "p":
127
+ print_mode = True
128
+ else:
129
+ return _result("", f"bash: shopt: -{c}: invalid option\n", 1)
130
+ else:
131
+ optnames.append(arg)
132
+ i += 1
133
+
134
+ shopts = _get_shopts(ctx)
135
+
136
+ # If both -s and -u specified, error
137
+ if set_mode and unset_mode:
138
+ return _result("", "bash: shopt: cannot set and unset options simultaneously\n", 1)
139
+
140
+ # No optnames and no -s/-u: show all options or those matching filter
141
+ if not optnames and not set_mode and not unset_mode:
142
+ output = []
143
+ for name in sorted(DEFAULT_SHOPTS.keys()):
144
+ state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
145
+ state_str = "on" if state else "off"
146
+ if print_mode:
147
+ prefix = "-s" if state else "-u"
148
+ output.append(f"shopt {prefix} {name}")
149
+ else:
150
+ output.append(f"{name}\t{state_str}")
151
+ if not quiet_mode:
152
+ return _result("\n".join(output) + "\n" if output else "", "", 0)
153
+ return _result("", "", 0)
154
+
155
+ # With -s/-u but no optnames: show options that match the state
156
+ if not optnames and (set_mode or unset_mode) and not quiet_mode:
157
+ target_state = set_mode
158
+ output = []
159
+ for name in sorted(DEFAULT_SHOPTS.keys()):
160
+ state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
161
+ if state == target_state:
162
+ state_str = "on" if state else "off"
163
+ if print_mode:
164
+ prefix = "-s" if state else "-u"
165
+ output.append(f"shopt {prefix} {name}")
166
+ else:
167
+ output.append(f"{name}\t{state_str}")
168
+ return _result("\n".join(output) + "\n" if output else "", "", 0)
169
+
170
+ # Process optnames
171
+ exit_code = 0
172
+ output = []
173
+
174
+ for name in optnames:
175
+ # Check if option exists
176
+ if name not in DEFAULT_SHOPTS:
177
+ # Unknown option - but if ignore_shopt_not_impl is set, silently ignore
178
+ if shopts.get("ignore_shopt_not_impl", False):
179
+ continue
180
+ return _result("", f"bash: shopt: {name}: invalid shell option name\n", 1)
181
+
182
+ if set_mode:
183
+ _set_shopt(ctx, name, True)
184
+ elif unset_mode:
185
+ _set_shopt(ctx, name, False)
186
+ else:
187
+ # Query mode
188
+ state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
189
+ if not state:
190
+ exit_code = 1
191
+ if not quiet_mode:
192
+ state_str = "on" if state else "off"
193
+ if print_mode:
194
+ prefix = "-s" if state else "-u"
195
+ output.append(f"shopt {prefix} {name}")
196
+ else:
197
+ output.append(f"{name}\t{state_str}")
198
+
199
+ if output and not quiet_mode:
200
+ return _result("\n".join(output) + "\n", "", exit_code)
201
+ return _result("", "", exit_code)
@@ -0,0 +1,136 @@
1
+ """Source and eval builtin implementations.
2
+
3
+ source (or .) - Execute commands from a file in the current shell.
4
+
5
+ Usage: source filename [arguments]
6
+ . filename [arguments]
7
+
8
+ Read and execute commands from filename in the current shell environment.
9
+ If arguments are supplied, they become the positional parameters when
10
+ filename is executed.
11
+
12
+ eval - Construct a command by concatenating arguments.
13
+
14
+ Usage: eval [arg ...]
15
+
16
+ Concatenate arguments into a single command, then execute.
17
+ """
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from ..types import InterpreterContext
23
+ from ...types import ExecResult
24
+
25
+
26
+ async def handle_source(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
27
+ """Execute the source / . builtin."""
28
+ from ...types import ExecResult
29
+
30
+ if not args:
31
+ return ExecResult(
32
+ stdout="",
33
+ stderr="bash: source: filename argument required\n",
34
+ exit_code=2,
35
+ )
36
+
37
+ filename = args[0]
38
+ script_args = args[1:]
39
+
40
+ # Resolve path
41
+ path = ctx.fs.resolve_path(ctx.state.cwd, filename)
42
+
43
+ # Read file
44
+ try:
45
+ content = await ctx.fs.read_file(path)
46
+ except FileNotFoundError:
47
+ return ExecResult(
48
+ stdout="",
49
+ stderr=f"bash: source: {filename}: No such file or directory\n",
50
+ exit_code=1,
51
+ )
52
+ except Exception as e:
53
+ return ExecResult(
54
+ stdout="",
55
+ stderr=f"bash: source: {filename}: {e}\n",
56
+ exit_code=1,
57
+ )
58
+
59
+ # Check source depth limit
60
+ ctx.state.source_depth += 1
61
+ if ctx.state.source_depth > ctx.limits.max_call_depth:
62
+ ctx.state.source_depth -= 1
63
+ return ExecResult(
64
+ stdout="",
65
+ stderr="bash: source: maximum source depth exceeded\n",
66
+ exit_code=1,
67
+ )
68
+
69
+ # Save and set positional parameters if arguments provided
70
+ saved_params = {}
71
+ saved_count = None
72
+ if script_args:
73
+ # Save current positional parameters
74
+ i = 1
75
+ while str(i) in ctx.state.env:
76
+ saved_params[str(i)] = ctx.state.env[str(i)]
77
+ i += 1
78
+ saved_count = ctx.state.env.get("#", "0")
79
+
80
+ # Clear and set new positional parameters
81
+ i = 1
82
+ while str(i) in ctx.state.env:
83
+ del ctx.state.env[str(i)]
84
+ i += 1
85
+
86
+ for i, arg in enumerate(script_args, start=1):
87
+ ctx.state.env[str(i)] = arg
88
+ ctx.state.env["#"] = str(len(script_args))
89
+
90
+ try:
91
+ # Parse and execute
92
+ from ...parser import parse
93
+
94
+ ast = parse(content)
95
+ result = await ctx.execute_script(ast)
96
+ return result
97
+ finally:
98
+ ctx.state.source_depth -= 1
99
+
100
+ # Restore positional parameters if we changed them
101
+ if saved_count is not None:
102
+ # Clear current positional parameters
103
+ i = 1
104
+ while str(i) in ctx.state.env:
105
+ del ctx.state.env[str(i)]
106
+ i += 1
107
+
108
+ # Restore saved parameters
109
+ for k, v in saved_params.items():
110
+ ctx.state.env[k] = v
111
+ ctx.state.env["#"] = saved_count
112
+
113
+
114
+ async def handle_eval(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
115
+ """Execute the eval builtin."""
116
+ from ...types import ExecResult
117
+
118
+ if not args:
119
+ return ExecResult(stdout="", stderr="", exit_code=0)
120
+
121
+ # Concatenate arguments with spaces
122
+ command = " ".join(args)
123
+
124
+ # Parse and execute
125
+ try:
126
+ from ...parser import parse
127
+
128
+ ast = parse(command)
129
+ result = await ctx.execute_script(ast)
130
+ return result
131
+ except Exception as e:
132
+ return ExecResult(
133
+ stdout="",
134
+ stderr=f"bash: eval: {e}\n",
135
+ exit_code=1,
136
+ )