shellsim 0.1.5__tar.gz → 0.1.8__tar.gz

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 (181) hide show
  1. {shellsim-0.1.5 → shellsim-0.1.8}/Cargo.lock +1 -1
  2. {shellsim-0.1.5 → shellsim-0.1.8}/Cargo.toml +1 -1
  3. {shellsim-0.1.5 → shellsim-0.1.8}/PKG-INFO +5 -3
  4. {shellsim-0.1.5 → shellsim-0.1.8}/README.md +4 -2
  5. {shellsim-0.1.5 → shellsim-0.1.8}/python/native/Cargo.lock +2 -2
  6. {shellsim-0.1.5 → shellsim-0.1.8}/python/native/Cargo.toml +1 -1
  7. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/_cli.py +4 -1
  8. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/builtins.rs +120 -1
  9. {shellsim-0.1.5 → shellsim-0.1.8}/src/interp.rs +21 -0
  10. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/heap.rs +41 -0
  11. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/object_model.rs +1 -0
  12. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/bisect.rs +109 -11
  13. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/core.rs +589 -1
  14. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/frozen.rs +5 -0
  15. shellsim-0.1.8/src/python/stdlib/itertools.rs +344 -0
  16. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/math.rs +319 -0
  17. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/mod.rs +1 -0
  18. shellsim-0.1.8/src/python/stdlib/source/cmath.py +65 -0
  19. shellsim-0.1.8/src/python/stdlib/source/complex.py +112 -0
  20. shellsim-0.1.8/src/python/stdlib/source/copy.py +29 -0
  21. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/pathlib.py +30 -0
  22. shellsim-0.1.8/src/python/stdlib/source/shutil.py +57 -0
  23. shellsim-0.1.8/src/python/stdlib/source/textwrap.py +73 -0
  24. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/calls.rs +165 -4
  25. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/namespace.rs +18 -0
  26. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm.rs +8 -0
  27. {shellsim-0.1.5 → shellsim-0.1.8}/src/vfs.rs +163 -41
  28. shellsim-0.1.5/src/python/stdlib/itertools.rs +0 -118
  29. {shellsim-0.1.5 → shellsim-0.1.8}/LICENSE +0 -0
  30. {shellsim-0.1.5 → shellsim-0.1.8}/pyproject.toml +0 -0
  31. {shellsim-0.1.5 → shellsim-0.1.8}/python/native/src/lib.rs +0 -0
  32. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/__init__.py +0 -0
  33. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/__main__.py +0 -0
  34. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/_api.py +0 -0
  35. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/py.typed +0 -0
  36. {shellsim-0.1.5 → shellsim-0.1.8}/python/shellsim/python.py +0 -0
  37. {shellsim-0.1.5 → shellsim-0.1.8}/rust-toolchain.toml +0 -0
  38. {shellsim-0.1.5 → shellsim-0.1.8}/src/bin/shellsim-python.rs +0 -0
  39. {shellsim-0.1.5 → shellsim-0.1.8}/src/clock.rs +0 -0
  40. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/archives.rs +0 -0
  41. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/awk/ast.rs +0 -0
  42. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/awk/eval.rs +0 -0
  43. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/awk/lexer.rs +0 -0
  44. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/awk/parser.rs +0 -0
  45. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/awk.rs +0 -0
  46. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/dd.rs +0 -0
  47. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/echo.rs +0 -0
  48. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/find.rs +0 -0
  49. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/fs.rs +0 -0
  50. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/apply.rs +0 -0
  51. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/commit.rs +0 -0
  52. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/compare.rs +0 -0
  53. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/config.rs +0 -0
  54. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/conflict.rs +0 -0
  55. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/diff.rs +0 -0
  56. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/ignore.rs +0 -0
  57. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/import.rs +0 -0
  58. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/log.rs +0 -0
  59. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/merge.rs +0 -0
  60. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/mod.rs +0 -0
  61. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/plumbing.rs +0 -0
  62. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/rebase.rs +0 -0
  63. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/refs.rs +0 -0
  64. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/repo.rs +0 -0
  65. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/stash.rs +0 -0
  66. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/switch.rs +0 -0
  67. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/git/worktree.rs +0 -0
  68. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/hashing.rs +0 -0
  69. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/makecmd.rs +0 -0
  70. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/mod.rs +0 -0
  71. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/net.rs +0 -0
  72. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/options.rs +0 -0
  73. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/patch.rs +0 -0
  74. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/pkg.rs +0 -0
  75. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/printf.rs +0 -0
  76. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/proc.rs +0 -0
  77. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/regex_compat.rs +0 -0
  78. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/rgcmd.rs +0 -0
  79. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/sed.rs +0 -0
  80. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/sort.rs +0 -0
  81. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/system.rs +0 -0
  82. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/tarcmd.rs +0 -0
  83. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/text.rs +0 -0
  84. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/unavailable.rs +0 -0
  85. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/util.rs +0 -0
  86. {shellsim-0.1.5 → shellsim-0.1.8}/src/commands/zipcmd.rs +0 -0
  87. {shellsim-0.1.5 → shellsim-0.1.8}/src/descriptors.rs +0 -0
  88. {shellsim-0.1.5 → shellsim-0.1.8}/src/exec.rs +0 -0
  89. {shellsim-0.1.5 → shellsim-0.1.8}/src/expand.rs +0 -0
  90. {shellsim-0.1.5 → shellsim-0.1.8}/src/harness.rs +0 -0
  91. {shellsim-0.1.5 → shellsim-0.1.8}/src/harness_manager.rs +0 -0
  92. {shellsim-0.1.5 → shellsim-0.1.8}/src/hashes.rs +0 -0
  93. {shellsim-0.1.5 → shellsim-0.1.8}/src/host_ingest.rs +0 -0
  94. {shellsim-0.1.5 → shellsim-0.1.8}/src/jqcmd.rs +0 -0
  95. {shellsim-0.1.5 → shellsim-0.1.8}/src/lib.rs +0 -0
  96. {shellsim-0.1.5 → shellsim-0.1.8}/src/main.rs +0 -0
  97. {shellsim-0.1.5 → shellsim-0.1.8}/src/mcp.rs +0 -0
  98. {shellsim-0.1.5 → shellsim-0.1.8}/src/net.rs +0 -0
  99. {shellsim-0.1.5 → shellsim-0.1.8}/src/netcmd.rs +0 -0
  100. {shellsim-0.1.5 → shellsim-0.1.8}/src/process.rs +0 -0
  101. {shellsim-0.1.5 → shellsim-0.1.8}/src/pseudo_fs.rs +0 -0
  102. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/ast.rs +0 -0
  103. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/bytecode.rs +0 -0
  104. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/compiler.rs +0 -0
  105. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/filesystem.rs +0 -0
  106. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/http.rs +0 -0
  107. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/lexer.rs +0 -0
  108. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/mapping.rs +0 -0
  109. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/mod.rs +0 -0
  110. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/native.rs +0 -0
  111. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/number.rs +0 -0
  112. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/parser.rs +0 -0
  113. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/process.rs +0 -0
  114. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/protocol.rs +0 -0
  115. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/slice.rs +0 -0
  116. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/source.rs +0 -0
  117. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/argparse.rs +0 -0
  118. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/base64.rs +0 -0
  119. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/collections.rs +0 -0
  120. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/dataclasses.rs +0 -0
  121. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/enum.rs +0 -0
  122. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/functools.rs +0 -0
  123. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/hashlib.rs +0 -0
  124. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/heapq.rs +0 -0
  125. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/http.rs +0 -0
  126. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/importlib.rs +0 -0
  127. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/json.rs +0 -0
  128. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/numpy.rs +0 -0
  129. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/os.rs +0 -0
  130. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/pytest.rs +0 -0
  131. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/re.rs +0 -0
  132. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/abc.py +0 -0
  133. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/base64.py +0 -0
  134. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/codecs.py +0 -0
  135. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/collections.py +0 -0
  136. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/csv.py +0 -0
  137. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/datetime.py +0 -0
  138. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/glob.py +0 -0
  139. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/hashlib.py +0 -0
  140. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/http.py +0 -0
  141. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/http_client.py +0 -0
  142. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/importlib.py +0 -0
  143. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/io.py +0 -0
  144. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/json.py +0 -0
  145. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/logging.py +0 -0
  146. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/numpy_random.py +0 -0
  147. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/os.py +0 -0
  148. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/random.py +0 -0
  149. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/statistics.py +0 -0
  150. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/struct.py +0 -0
  151. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/subprocess.py +0 -0
  152. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/tempfile.py +0 -0
  153. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/urllib.py +0 -0
  154. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/urllib_error.py +0 -0
  155. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/urllib_request.py +0 -0
  156. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/uuid.py +0 -0
  157. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/zipfile.py +0 -0
  158. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/source/zlib.py +0 -0
  159. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/string.rs +0 -0
  160. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/struct.rs +0 -0
  161. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/subprocess.rs +0 -0
  162. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/sys.rs +0 -0
  163. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/time.rs +0 -0
  164. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/typing.rs +0 -0
  165. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/unittest.rs +0 -0
  166. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/vfs.rs +0 -0
  167. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/stdlib/zlib.rs +0 -0
  168. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/string.rs +0 -0
  169. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/token.rs +0 -0
  170. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/dispatch.rs +0 -0
  171. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/host.rs +0 -0
  172. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/iteration.rs +0 -0
  173. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/native_runtime.rs +0 -0
  174. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/objects.rs +0 -0
  175. {shellsim-0.1.5 → shellsim-0.1.8}/src/python/vm/operations.rs +0 -0
  176. {shellsim-0.1.5 → shellsim-0.1.8}/src/resources.rs +0 -0
  177. {shellsim-0.1.5 → shellsim-0.1.8}/src/sandbox.rs +0 -0
  178. {shellsim-0.1.5 → shellsim-0.1.8}/src/scenario.rs +0 -0
  179. {shellsim-0.1.5 → shellsim-0.1.8}/src/scheduler.rs +0 -0
  180. {shellsim-0.1.5 → shellsim-0.1.8}/src/shell.rs +0 -0
  181. {shellsim-0.1.5 → shellsim-0.1.8}/src/telemetry.rs +0 -0
@@ -330,7 +330,7 @@ dependencies = [
330
330
 
331
331
  [[package]]
332
332
  name = "shellsim"
333
- version = "0.1.5"
333
+ version = "0.1.8"
334
334
  dependencies = [
335
335
  "base64",
336
336
  "crc32fast",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shellsim"
3
- version = "0.1.5"
3
+ version = "0.1.8"
4
4
  edition = "2021"
5
5
  description = "In-process BusyBox-like environment for RL rollouts and agent tests"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shellsim
3
- Version: 0.1.5
3
+ Version: 0.1.8
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: Apache Software License
6
6
  Classifier: Operating System :: MacOS :: MacOS X
@@ -160,8 +160,10 @@ printf '%s\n' \
160
160
 
161
161
  CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
162
162
  usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
163
- 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
164
- Exhaustion is observable and never falls back to an ambient host implementation.
163
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs use simple, stable approximations intended to
164
+ be correct within an order of magnitude. They bound runaway work and make runs comparable; they
165
+ do not model allocator layouts or instruction timing exactly. Exhaustion is observable and never
166
+ falls back to an ambient host implementation.
165
167
 
166
168
  For internals and contribution workflow, see [implementation](docs/implementation.md),
167
169
  [Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -137,8 +137,10 @@ printf '%s\n' \
137
137
 
138
138
  CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
139
139
  usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
140
- 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
141
- Exhaustion is observable and never falls back to an ambient host implementation.
140
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs use simple, stable approximations intended to
141
+ be correct within an order of magnitude. They bound runaway work and make runs comparable; they
142
+ do not model allocator layouts or instruction timing exactly. Exhaustion is observable and never
143
+ falls back to an ambient host implementation.
142
144
 
143
145
  For internals and contribution workflow, see [implementation](docs/implementation.md),
144
146
  [Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -406,7 +406,7 @@ dependencies = [
406
406
 
407
407
  [[package]]
408
408
  name = "shellsim"
409
- version = "0.1.5"
409
+ version = "0.1.8"
410
410
  dependencies = [
411
411
  "base64",
412
412
  "crc32fast",
@@ -427,7 +427,7 @@ dependencies = [
427
427
 
428
428
  [[package]]
429
429
  name = "shellsim-python-bindings"
430
- version = "0.1.5"
430
+ version = "0.1.8"
431
431
  dependencies = [
432
432
  "pyo3",
433
433
  "serde",
@@ -2,7 +2,7 @@
2
2
 
3
3
  [package]
4
4
  name = "shellsim-python-bindings"
5
- version = "0.1.5"
5
+ version = "0.1.8"
6
6
  edition = "2021"
7
7
  description = "Python bindings for shellsim"
8
8
  license = "Apache-2.0"
@@ -148,7 +148,10 @@ def _interactive(environment: Environment, stdin: Any, stdout: Any, stderr: Any)
148
148
  def main(arguments: Optional[Sequence[str]] = None) -> int:
149
149
  """Run the package CLI and return the simulated or usage exit status."""
150
150
 
151
- options = _parser().parse_args(sys.argv[1:] if arguments is None else arguments)
151
+ parsed_arguments = list(sys.argv[1:] if arguments is None else arguments)
152
+ if parsed_arguments[-1:] == ["--"]:
153
+ parsed_arguments.pop()
154
+ options = _parser().parse_args(parsed_arguments)
152
155
  environment, preparation_status = _prepare_environment(options, sys.stdout, sys.stderr)
153
156
  if environment is None:
154
157
  return preparation_status
@@ -50,7 +50,7 @@ pub fn register(m: &mut HashMap<&'static str, CommandSpec>) {
50
50
  reg_resumable(m, &["command"], Trust::Real, cmd_command, start_command);
51
51
  reg(m, &["alias"], Trust::Partial, cmd_alias);
52
52
  reg(m, &["unalias"], Trust::Real, cmd_unalias);
53
- reg_unsupported(m, &["getopts"]);
53
+ reg(m, &["getopts"], Trust::Real, cmd_getopts);
54
54
  reg(m, &["let"], Trust::Real, cmd_let);
55
55
  reg(m, &["mapfile", "readarray"], Trust::Real, cmd_mapfile);
56
56
  reg(m, &["pushd"], Trust::Real, cmd_pushd);
@@ -1259,6 +1259,125 @@ fn cmd_shift(interp: &mut CommandContext<'_>, args: &[String], _io: &mut Io) ->
1259
1259
  0
1260
1260
  }
1261
1261
 
1262
+ fn cmd_getopts(interp: &mut CommandContext<'_>, args: &[String], io: &mut Io) -> i32 {
1263
+ if args.len() < 2 {
1264
+ ewln(io.err, "getopts: usage: getopts optstring name [arg ...]");
1265
+ return 2;
1266
+ }
1267
+ let optstring = &args[0];
1268
+ let name = &args[1];
1269
+ let operands = if args.len() > 2 {
1270
+ args[2..].to_vec()
1271
+ } else {
1272
+ interp.positional.clone()
1273
+ };
1274
+ let visible_optind = interp
1275
+ .get_var("OPTIND")
1276
+ .and_then(|value| value.parse::<usize>().ok())
1277
+ .filter(|value| *value > 0)
1278
+ .unwrap_or(1);
1279
+ if visible_optind != interp.getopts.optind {
1280
+ interp.getopts.optind = visible_optind;
1281
+ interp.getopts.offset = 1;
1282
+ }
1283
+
1284
+ let argument_index = interp.getopts.optind - 1;
1285
+ let Some(argument) = operands.get(argument_index) else {
1286
+ set_getopts_optind(interp);
1287
+ return 1;
1288
+ };
1289
+ if argument == "--" {
1290
+ interp.getopts.optind += 1;
1291
+ interp.getopts.offset = 1;
1292
+ set_getopts_optind(interp);
1293
+ return 1;
1294
+ }
1295
+ let option_characters = argument.chars().collect::<Vec<_>>();
1296
+ if !argument.starts_with('-')
1297
+ || argument == "-"
1298
+ || interp.getopts.offset >= option_characters.len()
1299
+ {
1300
+ set_getopts_optind(interp);
1301
+ return 1;
1302
+ }
1303
+
1304
+ let option = option_characters[interp.getopts.offset];
1305
+ let silent = optstring.starts_with(':');
1306
+ let specification = optstring
1307
+ .trim_start_matches(':')
1308
+ .chars()
1309
+ .collect::<Vec<_>>();
1310
+ let Some(specification_index) = specification
1311
+ .iter()
1312
+ .position(|candidate| *candidate == option)
1313
+ else {
1314
+ advance_getopts(interp, option_characters.len());
1315
+ if silent {
1316
+ interp.set_var("OPTARG", option.to_string());
1317
+ } else {
1318
+ interp.vars.remove("OPTARG");
1319
+ }
1320
+ interp.set_var(name, "?");
1321
+ set_getopts_optind(interp);
1322
+ if !silent {
1323
+ ewln(io.err, &format!("getopts: illegal option -- {option}"));
1324
+ }
1325
+ return 0;
1326
+ };
1327
+ let requires_argument = specification.get(specification_index + 1) == Some(&':');
1328
+ if !requires_argument {
1329
+ advance_getopts(interp, option_characters.len());
1330
+ interp.vars.remove("OPTARG");
1331
+ interp.set_var(name, option.to_string());
1332
+ set_getopts_optind(interp);
1333
+ return 0;
1334
+ }
1335
+
1336
+ let inline = option_characters[interp.getopts.offset + 1..]
1337
+ .iter()
1338
+ .collect::<String>();
1339
+ let option_argument = if !inline.is_empty() {
1340
+ interp.getopts.optind += 1;
1341
+ inline
1342
+ } else if let Some(value) = operands.get(argument_index + 1) {
1343
+ interp.getopts.optind += 2;
1344
+ value.clone()
1345
+ } else {
1346
+ interp.getopts.optind += 1;
1347
+ interp.getopts.offset = 1;
1348
+ set_getopts_optind(interp);
1349
+ interp.set_var(name, if silent { ":" } else { "?" });
1350
+ if silent {
1351
+ interp.set_var("OPTARG", option.to_string());
1352
+ } else {
1353
+ interp.vars.remove("OPTARG");
1354
+ ewln(
1355
+ io.err,
1356
+ &format!("getopts: option requires an argument -- {option}"),
1357
+ );
1358
+ }
1359
+ return 0;
1360
+ };
1361
+ interp.getopts.offset = 1;
1362
+ interp.set_var("OPTARG", option_argument);
1363
+ interp.set_var(name, option.to_string());
1364
+ set_getopts_optind(interp);
1365
+ 0
1366
+ }
1367
+
1368
+ fn advance_getopts(interp: &mut CommandContext<'_>, argument_length: usize) {
1369
+ interp.getopts.offset += 1;
1370
+ if interp.getopts.offset >= argument_length {
1371
+ interp.getopts.optind += 1;
1372
+ interp.getopts.offset = 1;
1373
+ }
1374
+ }
1375
+
1376
+ fn set_getopts_optind(interp: &mut CommandContext<'_>) {
1377
+ let value = interp.getopts.optind.to_string();
1378
+ interp.vars.insert("OPTIND".to_string(), value);
1379
+ }
1380
+
1262
1381
  fn cmd_test(interp: &mut CommandContext<'_>, args: &[String], io: &mut Io) -> i32 {
1263
1382
  eval_test_cmd(interp, "test", args, io)
1264
1383
  }
@@ -24,6 +24,13 @@ pub enum ArrayVal {
24
24
  Assoc(BTreeMap<String, String>),
25
25
  }
26
26
 
27
+ /// Cursor retained between calls to the `getopts` shell builtin.
28
+ #[derive(Clone, Debug)]
29
+ pub(crate) struct GetoptsState {
30
+ pub(crate) optind: usize,
31
+ pub(crate) offset: usize,
32
+ }
33
+
27
34
  /// A simulated background job started with `&`.
28
35
  ///
29
36
  /// Its process continuation is owned separately by [`ProcessStates`]. This shell-facing record
@@ -144,6 +151,7 @@ pub struct ProcessState {
144
151
  random_state: Cell<u32>,
145
152
  /// positional parameters `$1 $2 ... $@`
146
153
  pub positional: Vec<String>,
154
+ pub(crate) getopts: GetoptsState,
147
155
  /// `set -e` / `set -u` / `set -x`
148
156
  pub opt_errexit: bool,
149
157
  pub opt_nounset: bool,
@@ -316,6 +324,7 @@ impl ProcessState {
316
324
  last_status: self.last_status,
317
325
  random_state: Cell::new(self.random_state.get()),
318
326
  positional: self.positional.clone(),
327
+ getopts: self.getopts.clone(),
319
328
  opt_errexit: self.opt_errexit,
320
329
  opt_nounset: self.opt_nounset,
321
330
  opt_xtrace: self.opt_xtrace,
@@ -550,6 +559,10 @@ impl Environment {
550
559
  last_status: 0,
551
560
  random_state: Cell::new(1),
552
561
  positional: Vec::new(),
562
+ getopts: GetoptsState {
563
+ optind: 1,
564
+ offset: 1,
565
+ },
553
566
  opt_errexit: false,
554
567
  opt_nounset: false,
555
568
  opt_xtrace: false,
@@ -1457,6 +1470,14 @@ impl Environment {
1457
1470
  if name == "PWD" {
1458
1471
  self.cwd = val.clone();
1459
1472
  }
1473
+ if name == "OPTIND" {
1474
+ self.getopts.optind = val
1475
+ .parse::<usize>()
1476
+ .ok()
1477
+ .filter(|value| *value > 0)
1478
+ .unwrap_or(1);
1479
+ self.getopts.offset = 1;
1480
+ }
1460
1481
  // A plain scalar assignment to an array name in bash sets element 0; we instead treat it
1461
1482
  // as a fresh scalar (drop the array) — the common case in our scripts and lower-risk.
1462
1483
  self.arrays.remove(name);
@@ -48,6 +48,10 @@ impl SymbolId {
48
48
  pub(super) const fn index(self) -> usize {
49
49
  self.0 as usize
50
50
  }
51
+
52
+ pub(super) fn from_index(index: usize) -> Option<Self> {
53
+ u32::try_from(index).ok().map(Self)
54
+ }
51
55
  }
52
56
 
53
57
  /// Runtime-local identity for one append-only instance storage layout.
@@ -386,6 +390,43 @@ impl Heap {
386
390
  self.symbol_ids.get(name).copied()
387
391
  }
388
392
 
393
+ /// Resolve a symbol identity back to its interpreter-owned name.
394
+ pub fn symbol_name(&self, symbol: SymbolId) -> Option<&str> {
395
+ self.symbol_names.get(symbol.index()).map(AsRef::as_ref)
396
+ }
397
+
398
+ /// Snapshot the names stored directly on an instance in either attribute representation.
399
+ pub fn instance_attribute_names(&self, id: ObjectId) -> Result<Vec<String>, String> {
400
+ let object = self
401
+ .objects
402
+ .get(id.0)
403
+ .and_then(Option::as_ref)
404
+ .ok_or("invalid object reference")?;
405
+ let Object::Instance { attributes, .. } = &object.payload else {
406
+ return Err("object does not have instance attributes".into());
407
+ };
408
+ match attributes {
409
+ InstanceAttributes::Shaped { shape, values } => (0..values.len())
410
+ .map(|slot| {
411
+ let symbol = self
412
+ .shape_attribute_at(*shape, slot)
413
+ .ok_or("invalid instance shape slot")?;
414
+ self.symbol_name(symbol)
415
+ .map(str::to_string)
416
+ .ok_or_else(|| "invalid instance attribute symbol".into())
417
+ })
418
+ .collect(),
419
+ InstanceAttributes::Dictionary(values) => values
420
+ .keys()
421
+ .map(|symbol| {
422
+ self.symbol_name(*symbol)
423
+ .map(str::to_string)
424
+ .ok_or_else(|| "invalid instance attribute symbol".into())
425
+ })
426
+ .collect(),
427
+ }
428
+ }
429
+
389
430
  /// Intern one identifier, charging its process-lifetime storage before mutation.
390
431
  pub fn intern_symbol(
391
432
  &mut self,
@@ -883,6 +883,7 @@ fn install_builtin_slots(types: &mut [PyType]) {
883
883
  slots.add = Some(intrinsic(super::stdlib::core::slot_string_add));
884
884
  slots.multiply = Some(intrinsic(super::stdlib::core::slot_string_multiply));
885
885
  slots.reflected_multiply = Some(intrinsic(super::stdlib::core::slot_string_multiply));
886
+ slots.remainder = Some(intrinsic(super::stdlib::core::slot_string_remainder));
886
887
 
887
888
  let slots = &mut types[BuiltinType::Bytes as usize].slots;
888
889
  slots.length = Some(unary(super::stdlib::core::slot_bytes_length));
@@ -8,32 +8,67 @@ use std::cmp::Ordering;
8
8
 
9
9
  use super::super::native::PyValue as Value;
10
10
  use super::super::native::{
11
- CallArgs, FunctionDef, ModuleDef, PyError, PyResult, PyRuntime, PySequence, PyValueCast,
11
+ CallArgs, FunctionDef, ModuleDef, PyError, PyList, PyResult, PyRuntime, PySequence, PyValueCast,
12
12
  };
13
13
 
14
14
  pub(super) static MODULE: ModuleDef = ModuleDef {
15
15
  name: "bisect",
16
- functions: &[FunctionDef {
17
- module: "bisect",
18
- name: "bisect_left",
19
- call: native_bisect_left,
20
- }],
16
+ functions: &[
17
+ FunctionDef {
18
+ module: "bisect",
19
+ name: "bisect_left",
20
+ call: native_bisect_left,
21
+ },
22
+ FunctionDef {
23
+ module: "bisect",
24
+ name: "bisect_right",
25
+ call: native_bisect_right,
26
+ },
27
+ FunctionDef {
28
+ module: "bisect",
29
+ name: "bisect",
30
+ call: native_bisect_right,
31
+ },
32
+ FunctionDef {
33
+ module: "bisect",
34
+ name: "insort_left",
35
+ call: native_insort_left,
36
+ },
37
+ FunctionDef {
38
+ module: "bisect",
39
+ name: "insort_right",
40
+ call: native_insort_right,
41
+ },
42
+ FunctionDef {
43
+ module: "bisect",
44
+ name: "insort",
45
+ call: native_insort_right,
46
+ },
47
+ ],
21
48
  values: &[],
22
49
  };
23
50
 
24
51
  fn native_bisect_left(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
25
- args.expect_positional("bisect_left", 2, 2)?;
26
- args.reject_keywords("bisect_left")?;
52
+ native_bisect(runtime, args, false)
53
+ }
54
+
55
+ fn native_bisect_right(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
56
+ native_bisect(runtime, args, true)
57
+ }
58
+
59
+ fn native_bisect(runtime: &mut dyn PyRuntime, args: CallArgs, right: bool) -> PyResult {
60
+ args.expect_positional("bisect", 2, 4)?;
61
+ args.reject_keywords("bisect")?;
27
62
  let values = args.positional()[0]
28
63
  .cast::<PySequence>(runtime)?
29
64
  .items(runtime)?;
30
65
  let needle = &args.positional()[1];
31
- let mut low = 0usize;
32
- let mut high = values.len();
66
+ let (mut low, mut high) = bisect_bounds(runtime, &args, values.len())?;
33
67
  while low < high {
34
68
  runtime.charge_cpu(1)?;
35
69
  let middle = low + (high - low) / 2;
36
- if runtime.compare(&values[middle], needle)? == Ordering::Less {
70
+ let ordering = runtime.compare(&values[middle], needle)?;
71
+ if ordering == Ordering::Less || (right && ordering == Ordering::Equal) {
37
72
  low = middle + 1;
38
73
  } else {
39
74
  high = middle;
@@ -44,6 +79,69 @@ fn native_bisect_left(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
44
79
  .map_err(|_| PyError::overflow_error("bisect result exceeds bounded integer range"))
45
80
  }
46
81
 
82
+ fn bisect_bounds(
83
+ runtime: &dyn PyRuntime,
84
+ args: &CallArgs,
85
+ length: usize,
86
+ ) -> PyResult<(usize, usize)> {
87
+ let low = args
88
+ .positional()
89
+ .get(2)
90
+ .map(|value| {
91
+ runtime
92
+ .int_value(value)
93
+ .ok_or_else(|| PyError::type_error("lo must be an integer"))
94
+ .and_then(|value| {
95
+ usize::try_from(value)
96
+ .map_err(|_| PyError::value_error("lo must be non-negative"))
97
+ })
98
+ })
99
+ .transpose()?
100
+ .unwrap_or(0);
101
+ let high = args
102
+ .positional()
103
+ .get(3)
104
+ .map(|value| {
105
+ runtime
106
+ .int_value(value)
107
+ .ok_or_else(|| PyError::type_error("hi must be an integer"))
108
+ .and_then(|value| {
109
+ usize::try_from(value)
110
+ .map_err(|_| PyError::value_error("hi must be non-negative"))
111
+ })
112
+ })
113
+ .transpose()?
114
+ .unwrap_or(length);
115
+ if high > length {
116
+ return Err(PyError::value_error("hi exceeds sequence length"));
117
+ }
118
+ Ok((low, high))
119
+ }
120
+
121
+ fn native_insort_left(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
122
+ native_insort(runtime, args, false)
123
+ }
124
+
125
+ fn native_insort_right(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
126
+ native_insort(runtime, args, true)
127
+ }
128
+
129
+ fn native_insort(runtime: &mut dyn PyRuntime, args: CallArgs, right: bool) -> PyResult {
130
+ let list = args
131
+ .positional()
132
+ .first()
133
+ .copied()
134
+ .ok_or_else(|| PyError::type_error("insort expected a list and a value"))?
135
+ .cast::<PyList>(runtime)?;
136
+ let index_value = native_bisect(runtime, args.clone(), right)?;
137
+ let index = runtime
138
+ .int_value(&index_value)
139
+ .and_then(|index| usize::try_from(index).ok())
140
+ .ok_or_else(|| PyError::runtime_error("bisect returned an invalid index"))?;
141
+ runtime.list_insert(list, index, args.positional()[1])?;
142
+ Ok(Value::None)
143
+ }
144
+
47
145
  /// Errors for an explicitly bounded search.
48
146
  #[cfg(test)]
49
147
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]