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,154 @@
1
+ """Alias builtin implementation.
2
+
3
+ Usage: alias [-p] [name[=value] ...]
4
+
5
+ Define or display aliases.
6
+
7
+ Options:
8
+ -p Print all aliases in reusable format
9
+ """
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from ..types import InterpreterContext
15
+ from ...types import ExecResult
16
+
17
+
18
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
19
+ """Create an ExecResult."""
20
+ from ...types import ExecResult
21
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
22
+
23
+
24
+ def get_aliases(ctx: "InterpreterContext") -> dict[str, str]:
25
+ """Get the aliases dictionary from context."""
26
+ # Store aliases in a special env variable as a serialized format
27
+ # Or use a dedicated attribute on state
28
+ if not hasattr(ctx.state, '_aliases'):
29
+ ctx.state._aliases = {}
30
+ return ctx.state._aliases
31
+
32
+
33
+ def set_alias(ctx: "InterpreterContext", name: str, value: str) -> None:
34
+ """Set an alias."""
35
+ aliases = get_aliases(ctx)
36
+ aliases[name] = value
37
+
38
+
39
+ def unset_alias(ctx: "InterpreterContext", name: str) -> bool:
40
+ """Unset an alias. Returns True if it existed."""
41
+ aliases = get_aliases(ctx)
42
+ if name in aliases:
43
+ del aliases[name]
44
+ return True
45
+ return False
46
+
47
+
48
+ async def handle_alias(
49
+ ctx: "InterpreterContext", args: list[str]
50
+ ) -> "ExecResult":
51
+ """Execute the alias builtin."""
52
+ # Parse options
53
+ print_format = False
54
+ names = []
55
+
56
+ i = 0
57
+ while i < len(args):
58
+ arg = args[i]
59
+ if arg == "-p":
60
+ print_format = True
61
+ elif arg == "--":
62
+ names.extend(args[i + 1:])
63
+ break
64
+ elif arg.startswith("-"):
65
+ return _result("", f"bash: alias: {arg}: invalid option\n", 1)
66
+ else:
67
+ names.append(arg)
68
+ i += 1
69
+
70
+ aliases = get_aliases(ctx)
71
+
72
+ # No arguments: show all aliases
73
+ if not names:
74
+ output = []
75
+ for name in sorted(aliases.keys()):
76
+ value = aliases[name]
77
+ output.append(f"alias {name}='{value}'")
78
+ if output:
79
+ return _result("\n".join(output) + "\n", "", 0)
80
+ return _result("", "", 0)
81
+
82
+ # Process arguments
83
+ output = []
84
+ exit_code = 0
85
+
86
+ for arg in names:
87
+ if "=" in arg:
88
+ # Define alias: name=value
89
+ eq_pos = arg.index("=")
90
+ name = arg[:eq_pos]
91
+ value = arg[eq_pos + 1:]
92
+ set_alias(ctx, name, value)
93
+ else:
94
+ # Show alias
95
+ name = arg
96
+ if name in aliases:
97
+ value = aliases[name]
98
+ output.append(f"alias {name}='{value}'")
99
+ else:
100
+ output.append(f"bash: alias: {name}: not found")
101
+ exit_code = 1
102
+
103
+ if output:
104
+ return _result("\n".join(output) + "\n", "", exit_code)
105
+ return _result("", "", exit_code)
106
+
107
+
108
+ async def handle_unalias(
109
+ ctx: "InterpreterContext", args: list[str]
110
+ ) -> "ExecResult":
111
+ """Execute the unalias builtin.
112
+
113
+ Usage: unalias [-a] name [name ...]
114
+
115
+ Options:
116
+ -a Remove all aliases
117
+ """
118
+ # Parse options
119
+ remove_all = False
120
+ names = []
121
+
122
+ i = 0
123
+ while i < len(args):
124
+ arg = args[i]
125
+ if arg == "-a":
126
+ remove_all = True
127
+ elif arg == "--":
128
+ names.extend(args[i + 1:])
129
+ break
130
+ elif arg.startswith("-"):
131
+ return _result("", f"bash: unalias: {arg}: invalid option\n", 1)
132
+ else:
133
+ names.append(arg)
134
+ i += 1
135
+
136
+ aliases = get_aliases(ctx)
137
+
138
+ if remove_all:
139
+ aliases.clear()
140
+ return _result("", "", 0)
141
+
142
+ if not names:
143
+ return _result("", "bash: unalias: usage: unalias [-a] name [name ...]\n", 1)
144
+
145
+ exit_code = 0
146
+ stderr_parts = []
147
+
148
+ for name in names:
149
+ if not unset_alias(ctx, name):
150
+ stderr_parts.append(f"bash: unalias: {name}: not found")
151
+ exit_code = 1
152
+
153
+ stderr = "\n".join(stderr_parts) + "\n" if stderr_parts else ""
154
+ return _result("", stderr, exit_code)
@@ -0,0 +1,76 @@
1
+ """Cd builtin implementation.
2
+
3
+ Usage: cd [dir]
4
+ cd -
5
+
6
+ Change the current working directory to dir. If dir is not specified,
7
+ change to $HOME. If dir is -, change to $OLDPWD.
8
+ """
9
+
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_cd(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
18
+ """Execute the cd builtin."""
19
+ from ...types import ExecResult
20
+
21
+ # Determine target directory
22
+ if not args:
23
+ # cd with no args goes to HOME
24
+ target = ctx.state.env.get("HOME", "/")
25
+ elif args[0] == "-":
26
+ # cd - goes to previous directory
27
+ target = ctx.state.previous_dir
28
+ if not target:
29
+ return ExecResult(
30
+ stdout="",
31
+ stderr="bash: cd: OLDPWD not set\n",
32
+ exit_code=1,
33
+ )
34
+ else:
35
+ target = args[0]
36
+
37
+ # Resolve the path
38
+ new_dir = ctx.fs.resolve_path(ctx.state.cwd, target)
39
+
40
+ # Verify directory exists
41
+ try:
42
+ exists = await ctx.fs.exists(new_dir)
43
+ if not exists:
44
+ return ExecResult(
45
+ stdout="",
46
+ stderr=f"bash: cd: {target}: No such file or directory\n",
47
+ exit_code=1,
48
+ )
49
+
50
+ is_dir = await ctx.fs.is_directory(new_dir)
51
+ if not is_dir:
52
+ return ExecResult(
53
+ stdout="",
54
+ stderr=f"bash: cd: {target}: Not a directory\n",
55
+ exit_code=1,
56
+ )
57
+ except Exception as e:
58
+ return ExecResult(
59
+ stdout="",
60
+ stderr=f"bash: cd: {target}: {e}\n",
61
+ exit_code=1,
62
+ )
63
+
64
+ # Update state
65
+ old_dir = ctx.state.cwd
66
+ ctx.state.previous_dir = old_dir
67
+ ctx.state.cwd = new_dir
68
+ ctx.state.env["OLDPWD"] = old_dir
69
+ ctx.state.env["PWD"] = new_dir
70
+
71
+ # If cd - was used, print the new directory
72
+ stdout = ""
73
+ if args and args[0] == "-":
74
+ stdout = new_dir + "\n"
75
+
76
+ return ExecResult(stdout=stdout, stderr="", exit_code=0)
@@ -0,0 +1,127 @@
1
+ """Control flow builtins: break, continue, return, exit.
2
+
3
+ These builtins control the flow of script execution.
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
+ from ..errors import BreakError, ContinueError, ReturnError, ExitError
13
+
14
+
15
+ async def handle_break(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
16
+ """Execute the break builtin.
17
+
18
+ Usage: break [n]
19
+
20
+ Exit from within a for, while, until, or select loop.
21
+ If n is specified, break out of n enclosing loops.
22
+ """
23
+ levels = 1
24
+ if args:
25
+ try:
26
+ levels = int(args[0])
27
+ if levels < 1:
28
+ levels = 1
29
+ except ValueError:
30
+ from ...types import ExecResult
31
+ return ExecResult(
32
+ stdout="",
33
+ stderr=f"bash: break: {args[0]}: numeric argument required\n",
34
+ exit_code=1,
35
+ )
36
+
37
+ # Check if we're in a loop
38
+ if ctx.state.loop_depth < 1:
39
+ from ...types import ExecResult
40
+ return ExecResult(
41
+ stdout="",
42
+ stderr="", # bash doesn't print error for break outside loop
43
+ exit_code=0,
44
+ )
45
+
46
+ raise BreakError(levels=levels)
47
+
48
+
49
+ async def handle_continue(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
50
+ """Execute the continue builtin.
51
+
52
+ Usage: continue [n]
53
+
54
+ Resume the next iteration of an enclosing for, while, until, or select loop.
55
+ If n is specified, resume at the nth enclosing loop.
56
+ """
57
+ levels = 1
58
+ if args:
59
+ try:
60
+ levels = int(args[0])
61
+ if levels < 1:
62
+ levels = 1
63
+ except ValueError:
64
+ from ...types import ExecResult
65
+ return ExecResult(
66
+ stdout="",
67
+ stderr=f"bash: continue: {args[0]}: numeric argument required\n",
68
+ exit_code=1,
69
+ )
70
+
71
+ # Check if we're in a loop
72
+ if ctx.state.loop_depth < 1:
73
+ from ...types import ExecResult
74
+ return ExecResult(
75
+ stdout="",
76
+ stderr="", # bash doesn't print error for continue outside loop
77
+ exit_code=0,
78
+ )
79
+
80
+ raise ContinueError(levels=levels)
81
+
82
+
83
+ async def handle_return(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
84
+ """Execute the return builtin.
85
+
86
+ Usage: return [n]
87
+
88
+ Return from a shell function or sourced script.
89
+ n is the return value (0-255). If n is omitted, the return value is
90
+ the exit status of the last command executed.
91
+ """
92
+ exit_code = ctx.state.last_exit_code
93
+ if args:
94
+ try:
95
+ exit_code = int(args[0]) & 255 # Mask to 0-255
96
+ except ValueError:
97
+ from ...types import ExecResult
98
+ return ExecResult(
99
+ stdout="",
100
+ stderr=f"bash: return: {args[0]}: numeric argument required\n",
101
+ exit_code=1,
102
+ )
103
+
104
+ raise ReturnError(exit_code)
105
+
106
+
107
+ async def handle_exit(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
108
+ """Execute the exit builtin.
109
+
110
+ Usage: exit [n]
111
+
112
+ Exit the shell with status n. If n is omitted, the exit status is
113
+ that of the last command executed.
114
+ """
115
+ exit_code = ctx.state.last_exit_code
116
+ if args:
117
+ try:
118
+ exit_code = int(args[0]) & 255 # Mask to 0-255
119
+ except ValueError:
120
+ from ...types import ExecResult
121
+ return ExecResult(
122
+ stdout="",
123
+ stderr=f"bash: exit: {args[0]}: numeric argument required\n",
124
+ exit_code=1,
125
+ )
126
+
127
+ raise ExitError(exit_code)
@@ -0,0 +1,336 @@
1
+ """Declare/typeset builtin implementation.
2
+
3
+ Usage: declare [-aAfFgiIlnrtux] [-p] [name[=value] ...]
4
+ typeset [-aAfFgiIlnrtux] [-p] [name[=value] ...]
5
+
6
+ Options:
7
+ -a indexed array
8
+ -A associative array
9
+ -f functions only
10
+ -F function names only
11
+ -g global scope (in function context)
12
+ -i integer attribute
13
+ -l lowercase
14
+ -n nameref
15
+ -p print declarations
16
+ -r readonly
17
+ -t trace (functions)
18
+ -u uppercase
19
+ -x export
20
+ """
21
+
22
+ import re
23
+ from typing import TYPE_CHECKING, Optional
24
+
25
+ if TYPE_CHECKING:
26
+ from ..types import InterpreterContext
27
+ from ...types import ExecResult
28
+
29
+
30
+ def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
31
+ """Create an ExecResult."""
32
+ from ...types import ExecResult
33
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
34
+
35
+
36
+ async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
37
+ """Execute the declare/typeset builtin."""
38
+ # Parse options
39
+ options = {
40
+ "array": False, # -a: indexed array
41
+ "assoc": False, # -A: associative array
42
+ "function": False, # -f: functions
43
+ "func_names": False, # -F: function names only
44
+ "global": False, # -g: global scope
45
+ "integer": False, # -i: integer
46
+ "lowercase": False, # -l: lowercase
47
+ "nameref": False, # -n: nameref
48
+ "print": False, # -p: print declarations
49
+ "readonly": False, # -r: readonly
50
+ "trace": False, # -t: trace
51
+ "uppercase": False, # -u: uppercase
52
+ "export": False, # -x: export
53
+ }
54
+
55
+ names: list[str] = []
56
+
57
+ i = 0
58
+ while i < len(args):
59
+ arg = args[i]
60
+
61
+ if arg == "--":
62
+ names.extend(args[i + 1:])
63
+ break
64
+
65
+ if arg.startswith("-") and len(arg) > 1 and arg[1] != "-":
66
+ # Parse short options
67
+ for c in arg[1:]:
68
+ if c == "a":
69
+ options["array"] = True
70
+ elif c == "A":
71
+ options["assoc"] = True
72
+ elif c == "f":
73
+ options["function"] = True
74
+ elif c == "F":
75
+ options["func_names"] = True
76
+ elif c == "g":
77
+ options["global"] = True
78
+ elif c == "i":
79
+ options["integer"] = True
80
+ elif c == "l":
81
+ options["lowercase"] = True
82
+ elif c == "n":
83
+ options["nameref"] = True
84
+ elif c == "p":
85
+ options["print"] = True
86
+ elif c == "r":
87
+ options["readonly"] = True
88
+ elif c == "t":
89
+ options["trace"] = True
90
+ elif c == "u":
91
+ options["uppercase"] = True
92
+ elif c == "x":
93
+ options["export"] = True
94
+ else:
95
+ return _result(
96
+ "",
97
+ f"bash: declare: -{c}: invalid option\n",
98
+ 2
99
+ )
100
+ else:
101
+ names.append(arg)
102
+
103
+ i += 1
104
+
105
+ # Print mode: show variable declarations
106
+ if options["print"]:
107
+ return _print_declarations(ctx, names, options)
108
+
109
+ # No names: list variables with matching attributes
110
+ if not names:
111
+ return _list_variables(ctx, options)
112
+
113
+ # Process each name/assignment
114
+ exit_code = 0
115
+ stderr_parts = []
116
+
117
+ for name_arg in names:
118
+ # Parse name and optional value
119
+ if "=" in name_arg:
120
+ eq_idx = name_arg.index("=")
121
+ name = name_arg[:eq_idx]
122
+ value_str = name_arg[eq_idx + 1:]
123
+ else:
124
+ name = name_arg
125
+ value_str = None
126
+
127
+ # Handle array subscript in name: arr[idx]
128
+ subscript = None
129
+ if "[" in name and name.endswith("]"):
130
+ bracket_idx = name.index("[")
131
+ subscript = name[bracket_idx + 1:-1]
132
+ name = name[:bracket_idx]
133
+
134
+ # Validate identifier
135
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
136
+ stderr_parts.append(f"bash: declare: `{name_arg}': not a valid identifier\n")
137
+ exit_code = 1
138
+ continue
139
+
140
+ # Handle array declaration
141
+ if options["array"] or options["assoc"]:
142
+ # Initialize array if not already set
143
+ array_key = f"{name}__is_array"
144
+ if array_key not in ctx.state.env:
145
+ ctx.state.env[array_key] = "assoc" if options["assoc"] else "indexed"
146
+
147
+ if value_str is not None:
148
+ # Parse array assignment: (a b c) or ([0]=a [1]=b)
149
+ if value_str.startswith("(") and value_str.endswith(")"):
150
+ inner = value_str[1:-1].strip()
151
+ _parse_array_assignment(ctx, name, inner, options["assoc"])
152
+ elif subscript is not None:
153
+ # arr[idx]=value
154
+ ctx.state.env[f"{name}_{subscript}"] = value_str
155
+ else:
156
+ # Simple value assignment to array[0]
157
+ ctx.state.env[f"{name}_0"] = value_str
158
+ else:
159
+ # Regular variable
160
+ if value_str is not None:
161
+ # Apply transformations
162
+ if options["integer"]:
163
+ # Evaluate as integer
164
+ try:
165
+ value_str = str(_eval_integer(value_str, ctx))
166
+ except Exception:
167
+ value_str = "0"
168
+
169
+ if options["lowercase"]:
170
+ value_str = value_str.lower()
171
+ elif options["uppercase"]:
172
+ value_str = value_str.upper()
173
+
174
+ if subscript is not None:
175
+ # Array element
176
+ ctx.state.env[f"{name}_{subscript}"] = value_str
177
+ else:
178
+ ctx.state.env[name] = value_str
179
+ elif name not in ctx.state.env:
180
+ # Declare without value - just set type info
181
+ if options["integer"]:
182
+ ctx.state.env[f"{name}__is_integer"] = "1"
183
+ if options["lowercase"]:
184
+ ctx.state.env[f"{name}__is_lower"] = "1"
185
+ if options["uppercase"]:
186
+ ctx.state.env[f"{name}__is_upper"] = "1"
187
+
188
+ return _result("", "".join(stderr_parts), exit_code)
189
+
190
+
191
+ def _parse_array_assignment(ctx: "InterpreterContext", name: str, inner: str, is_assoc: bool) -> None:
192
+ """Parse and assign array values from (a b c) or ([key]=value ...) syntax."""
193
+ # Clear existing array elements
194
+ to_remove = [k for k in ctx.state.env if k.startswith(f"{name}_") and not k.startswith(f"{name}__")]
195
+ for k in to_remove:
196
+ del ctx.state.env[k]
197
+
198
+ # Simple word splitting for now - doesn't handle all quoting cases
199
+ idx = 0
200
+ i = 0
201
+
202
+ while i < len(inner):
203
+ # Skip whitespace
204
+ while i < len(inner) and inner[i] in " \t":
205
+ i += 1
206
+
207
+ if i >= len(inner):
208
+ break
209
+
210
+ # Check for [key]=value syntax
211
+ if inner[i] == "[":
212
+ # Find closing bracket
213
+ j = i + 1
214
+ while j < len(inner) and inner[j] != "]":
215
+ j += 1
216
+ if j < len(inner) and j + 1 < len(inner) and inner[j + 1] == "=":
217
+ key = inner[i + 1:j]
218
+ # Find value
219
+ value_start = j + 2
220
+ value_end = value_start
221
+ in_quote = None
222
+ while value_end < len(inner):
223
+ c = inner[value_end]
224
+ if in_quote:
225
+ if c == in_quote:
226
+ in_quote = None
227
+ value_end += 1
228
+ elif c in "\"'":
229
+ in_quote = c
230
+ value_end += 1
231
+ elif c in " \t":
232
+ break
233
+ else:
234
+ value_end += 1
235
+
236
+ value = inner[value_start:value_end]
237
+ # Remove surrounding quotes if present
238
+ if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
239
+ value = value[1:-1]
240
+
241
+ ctx.state.env[f"{name}_{key}"] = value
242
+ i = value_end
243
+ continue
244
+
245
+ # Simple value - assign to next index
246
+ value_start = i
247
+ value_end = i
248
+ in_quote = None
249
+ while value_end < len(inner):
250
+ c = inner[value_end]
251
+ if in_quote:
252
+ if c == in_quote:
253
+ in_quote = None
254
+ value_end += 1
255
+ elif c in "\"'":
256
+ in_quote = c
257
+ value_end += 1
258
+ elif c in " \t":
259
+ break
260
+ else:
261
+ value_end += 1
262
+
263
+ value = inner[value_start:value_end]
264
+ # Remove surrounding quotes if present
265
+ if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
266
+ value = value[1:-1]
267
+
268
+ ctx.state.env[f"{name}_{idx}"] = value
269
+ idx += 1
270
+ i = value_end
271
+
272
+
273
+ def _eval_integer(expr: str, ctx: "InterpreterContext") -> int:
274
+ """Evaluate a simple integer expression."""
275
+ # Handle variable references
276
+ expr = expr.strip()
277
+
278
+ # Try direct integer
279
+ try:
280
+ return int(expr)
281
+ except ValueError:
282
+ pass
283
+
284
+ # Try variable reference
285
+ if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", expr):
286
+ val = ctx.state.env.get(expr, "0")
287
+ try:
288
+ return int(val)
289
+ except ValueError:
290
+ return 0
291
+
292
+ return 0
293
+
294
+
295
+ def _print_declarations(ctx: "InterpreterContext", names: list[str], options: dict) -> "ExecResult":
296
+ """Print variable declarations."""
297
+ lines = []
298
+
299
+ if not names:
300
+ # Print all matching variables
301
+ for name in sorted(ctx.state.env.keys()):
302
+ if name.startswith("_") or "__" in name:
303
+ continue
304
+ if name in ("?", "#", "$", "!", "-", "*", "@"):
305
+ continue
306
+
307
+ val = ctx.state.env[name]
308
+ lines.append(f'declare -- {name}="{val}"')
309
+ else:
310
+ for name in names:
311
+ if name in ctx.state.env:
312
+ val = ctx.state.env[name]
313
+ lines.append(f'declare -- {name}="{val}"')
314
+ else:
315
+ # Check if it's an array
316
+ is_array = ctx.state.env.get(f"{name}__is_array")
317
+ if is_array:
318
+ lines.append(f'declare -{("A" if is_array == "assoc" else "a")} {name}')
319
+
320
+ return _result("\n".join(lines) + "\n" if lines else "", "", 0)
321
+
322
+
323
+ def _list_variables(ctx: "InterpreterContext", options: dict) -> "ExecResult":
324
+ """List variables with matching attributes."""
325
+ lines = []
326
+
327
+ for name in sorted(ctx.state.env.keys()):
328
+ if name.startswith("_") or "__" in name:
329
+ continue
330
+ if name in ("?", "#", "$", "!", "-", "*", "@"):
331
+ continue
332
+
333
+ val = ctx.state.env[name]
334
+ lines.append(f'declare -- {name}="{val}"')
335
+
336
+ return _result("\n".join(lines) + "\n" if lines else "", "", 0)