@poe-platform/safe-bash 0.1.629 → 0.1.631

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 (377) hide show
  1. package/README.md +2 -2
  2. package/dist/office-package/compression.d.ts +19 -0
  3. package/dist/office-package/compression.js +186 -0
  4. package/dist/office-package/runtime.d.ts +11 -0
  5. package/dist/office-package/runtime.js +28 -0
  6. package/dist/safe-bash/chunks/{bz2-B2RXT4EX.js → bz2-ACU4EOVE.js} +2 -2
  7. package/dist/safe-bash/chunks/chunk-2E4KBFO6.js +239 -0
  8. package/dist/safe-bash/chunks/chunk-2E4KBFO6.js.map +7 -0
  9. package/dist/safe-bash/chunks/chunk-3E5M2WEL.js +33 -0
  10. package/dist/safe-bash/chunks/chunk-3E5M2WEL.js.map +7 -0
  11. package/dist/safe-bash/chunks/{chunk-WPEMUXV4.js → chunk-3LD67SER.js} +12 -10
  12. package/dist/safe-bash/chunks/{chunk-WPEMUXV4.js.map → chunk-3LD67SER.js.map} +1 -1
  13. package/dist/safe-bash/chunks/{chunk-6LSSEOG7.js → chunk-6HNK7Q66.js} +11 -7
  14. package/dist/safe-bash/chunks/{chunk-6LSSEOG7.js.map → chunk-6HNK7Q66.js.map} +1 -1
  15. package/dist/safe-bash/chunks/{chunk-AD7UAP5B.js → chunk-7EEWZY5S.js} +10 -8
  16. package/dist/safe-bash/chunks/{chunk-AD7UAP5B.js.map → chunk-7EEWZY5S.js.map} +1 -1
  17. package/dist/safe-bash/chunks/{chunk-RNKTWFEM.js → chunk-CDBQXRWL.js} +3 -3
  18. package/dist/safe-bash/chunks/{chunk-UYX72YHY.js → chunk-CHHAB5SS.js} +10 -74
  19. package/dist/safe-bash/chunks/{chunk-UYX72YHY.js.map → chunk-CHHAB5SS.js.map} +3 -3
  20. package/dist/safe-bash/chunks/{chunk-KYNRHYHP.js → chunk-EI4X5ETX.js} +15 -13
  21. package/dist/safe-bash/chunks/{chunk-KYNRHYHP.js.map → chunk-EI4X5ETX.js.map} +1 -1
  22. package/dist/safe-bash/chunks/{chunk-KZ7QCYNT.js → chunk-GIBLUW2U.js} +13 -9
  23. package/dist/safe-bash/chunks/{chunk-KZ7QCYNT.js.map → chunk-GIBLUW2U.js.map} +1 -1
  24. package/dist/safe-bash/chunks/chunk-GPV7ZFYS.js +1528 -0
  25. package/dist/safe-bash/chunks/chunk-GPV7ZFYS.js.map +7 -0
  26. package/dist/safe-bash/chunks/{chunk-N5CYTGBN.js → chunk-H3E67KTZ.js} +13 -9
  27. package/dist/safe-bash/chunks/{chunk-N5CYTGBN.js.map → chunk-H3E67KTZ.js.map} +1 -1
  28. package/dist/safe-bash/chunks/{chunk-AFH57PPL.js → chunk-IEKESMO4.js} +3 -3
  29. package/dist/safe-bash/chunks/{chunk-DPJWEGIX.js → chunk-JLBIUYT3.js} +2 -2
  30. package/dist/safe-bash/chunks/chunk-JZDO5MC3.js +77 -0
  31. package/dist/safe-bash/chunks/chunk-JZDO5MC3.js.map +7 -0
  32. package/dist/safe-bash/chunks/{chunk-GFZKQIPL.js → chunk-LA6BX562.js} +8 -6
  33. package/dist/safe-bash/chunks/{chunk-GFZKQIPL.js.map → chunk-LA6BX562.js.map} +1 -1
  34. package/dist/safe-bash/chunks/{chunk-PVJJFCGA.js → chunk-M3GPCYSP.js} +2 -2
  35. package/dist/safe-bash/chunks/{chunk-FWDMJTQO.js → chunk-NDCZ3UBP.js} +13 -9
  36. package/dist/safe-bash/chunks/{chunk-FWDMJTQO.js.map → chunk-NDCZ3UBP.js.map} +1 -1
  37. package/dist/safe-bash/chunks/{chunk-2KL6W6NB.js → chunk-O2EVBRKF.js} +1 -2
  38. package/dist/safe-bash/chunks/chunk-OFT77XIB.js +389 -0
  39. package/dist/safe-bash/chunks/chunk-OFT77XIB.js.map +7 -0
  40. package/dist/safe-bash/chunks/{chunk-PCLOK32J.js → chunk-PC6BEIAF.js} +13 -9
  41. package/dist/safe-bash/chunks/{chunk-PCLOK32J.js.map → chunk-PC6BEIAF.js.map} +1 -1
  42. package/dist/safe-bash/chunks/{chunk-3M5F7LD5.js → chunk-QWKP7ZU7.js} +13 -9
  43. package/dist/safe-bash/chunks/{chunk-3M5F7LD5.js.map → chunk-QWKP7ZU7.js.map} +1 -1
  44. package/dist/safe-bash/chunks/chunk-THXJ6CLF.js +53 -0
  45. package/dist/safe-bash/chunks/chunk-THXJ6CLF.js.map +7 -0
  46. package/dist/safe-bash/chunks/{chunk-HHJSJWUD.js → chunk-V73A742N.js} +57 -15
  47. package/dist/safe-bash/chunks/chunk-V73A742N.js.map +7 -0
  48. package/dist/safe-bash/chunks/{chunk-4BYUV5PA.js → chunk-VN3ZGME3.js} +12 -8
  49. package/dist/safe-bash/chunks/{chunk-4BYUV5PA.js.map → chunk-VN3ZGME3.js.map} +1 -1
  50. package/dist/safe-bash/chunks/chunk-WECHUK3Y.js +4285 -0
  51. package/dist/safe-bash/chunks/chunk-WECHUK3Y.js.map +7 -0
  52. package/dist/safe-bash/chunks/{chunk-X55AMTNM.js → chunk-WIOEUR7D.js} +14 -10
  53. package/dist/safe-bash/chunks/{chunk-X55AMTNM.js.map → chunk-WIOEUR7D.js.map} +1 -1
  54. package/dist/safe-bash/chunks/chunk-WPIOO4NX.js +443 -0
  55. package/dist/safe-bash/chunks/chunk-WPIOO4NX.js.map +7 -0
  56. package/dist/safe-bash/chunks/{chunk-C7QNJ3D4.js → chunk-WYTI64IU.js} +2 -2
  57. package/dist/safe-bash/chunks/{xz-AVXFDXW4.js → xz-U4QDTPZL.js} +2 -2
  58. package/dist/safe-bash/chunks/{zstd-R6RPHJYO.js → zstd-OG5RPYXO.js} +2 -2
  59. package/dist/safe-bash/commands/archive/zip/crc.d.ts +2 -0
  60. package/dist/safe-bash/commands/archive/zip/crc.d.ts.map +1 -0
  61. package/dist/safe-bash/commands/archive/zip/crc.js +7 -0
  62. package/dist/safe-bash/commands/archive/zip/crc.js.map +1 -0
  63. package/dist/safe-bash/commands/archive/zip/crypto.d.ts +4 -0
  64. package/dist/safe-bash/commands/archive/zip/crypto.d.ts.map +1 -0
  65. package/dist/safe-bash/commands/archive/zip/crypto.js +34 -0
  66. package/dist/safe-bash/commands/archive/zip/crypto.js.map +1 -0
  67. package/dist/safe-bash/commands/archive/zip/help.d.ts +3 -0
  68. package/dist/safe-bash/commands/archive/zip/help.d.ts.map +1 -0
  69. package/dist/safe-bash/commands/archive/zip/help.js +87 -0
  70. package/dist/safe-bash/commands/archive/zip/help.js.map +1 -0
  71. package/dist/safe-bash/commands/archive/zip/move.d.ts +12 -0
  72. package/dist/safe-bash/commands/archive/zip/move.d.ts.map +1 -0
  73. package/dist/safe-bash/commands/archive/zip/move.js +60 -0
  74. package/dist/safe-bash/commands/archive/zip/move.js.map +1 -0
  75. package/dist/safe-bash/commands/archive/zip/options.js +5 -5
  76. package/dist/safe-bash/commands/archive/zip/options.js.map +1 -1
  77. package/dist/safe-bash/commands/archive/zip-format.d.ts +1 -1
  78. package/dist/safe-bash/commands/archive/zip-format.d.ts.map +1 -1
  79. package/dist/safe-bash/commands/archive/zip-format.js +14 -17
  80. package/dist/safe-bash/commands/archive/zip-format.js.map +1 -1
  81. package/dist/safe-bash/commands/archive/zip.d.ts.map +1 -1
  82. package/dist/safe-bash/commands/archive/zip.js +45 -15
  83. package/dist/safe-bash/commands/archive/zip.js.map +1 -1
  84. package/dist/safe-bash/commands/bytes/compression/bounded-codec.d.ts +2 -0
  85. package/dist/safe-bash/commands/bytes/compression/bounded-codec.d.ts.map +1 -1
  86. package/dist/safe-bash/commands/bytes/compression/bounded-codec.js +4 -0
  87. package/dist/safe-bash/commands/bytes/compression/bounded-codec.js.map +1 -1
  88. package/dist/safe-bash/commands/bytes/compression/codec.d.ts +4 -24
  89. package/dist/safe-bash/commands/bytes/compression/codec.d.ts.map +1 -1
  90. package/dist/safe-bash/commands/bytes/compression/codec.js +12 -168
  91. package/dist/safe-bash/commands/bytes/compression/codec.js.map +1 -1
  92. package/dist/safe-bash/commands/cmp.js +1 -1
  93. package/dist/safe-bash/commands/cmp.js.map +1 -1
  94. package/dist/safe-bash/commands/csplit/index.browser.js +10 -7
  95. package/dist/safe-bash/commands/docx/index.browser.js +54 -0
  96. package/dist/safe-bash/commands/docx/index.browser.js.map +7 -0
  97. package/dist/safe-bash/commands/docx/index.d.ts +25 -0
  98. package/dist/safe-bash/commands/docx/index.d.ts.map +1 -0
  99. package/dist/safe-bash/commands/docx/index.js +42 -0
  100. package/dist/safe-bash/commands/docx/index.js.map +1 -0
  101. package/dist/safe-bash/commands/docx/io.d.ts +15 -0
  102. package/dist/safe-bash/commands/docx/io.d.ts.map +1 -0
  103. package/dist/safe-bash/commands/docx/io.js +16 -0
  104. package/dist/safe-bash/commands/docx/io.js.map +1 -0
  105. package/dist/safe-bash/commands/factor/index.browser.js +7 -5
  106. package/dist/safe-bash/commands/filesystem.d.ts.map +1 -1
  107. package/dist/safe-bash/commands/filesystem.js +9 -1
  108. package/dist/safe-bash/commands/filesystem.js.map +1 -1
  109. package/dist/safe-bash/commands/getopt/index.browser.js +7 -5
  110. package/dist/safe-bash/commands/hexdump/index.browser.js +8 -6
  111. package/dist/safe-bash/commands/iconv/index.browser.js +8 -6
  112. package/dist/safe-bash/commands/line-endings/index.browser.js +10 -7
  113. package/dist/safe-bash/commands/llm/index.browser.js +10 -8
  114. package/dist/safe-bash/commands/llm/providers/index.browser.js +3 -3
  115. package/dist/safe-bash/commands/network/index.browser.js +11 -8
  116. package/dist/safe-bash/commands/node/browser.d.ts +2 -0
  117. package/dist/safe-bash/commands/node/browser.d.ts.map +1 -0
  118. package/dist/safe-bash/commands/node/browser.js +2 -0
  119. package/dist/safe-bash/commands/node/browser.js.map +1 -0
  120. package/dist/safe-bash/commands/node/index.browser.js +18 -0
  121. package/dist/safe-bash/commands/node/index.browser.js.map +7 -0
  122. package/dist/safe-bash/commands/node/safejs.d.ts +6 -0
  123. package/dist/safe-bash/commands/node/safejs.d.ts.map +1 -1
  124. package/dist/safe-bash/commands/node/safejs.js +25 -1
  125. package/dist/safe-bash/commands/node/safejs.js.map +1 -1
  126. package/dist/safe-bash/commands/op/index.browser.js +3 -3
  127. package/dist/safe-bash/commands/op/index.js +0 -1
  128. package/dist/safe-bash/commands/op/index.js.map +2 -2
  129. package/dist/safe-bash/commands/pptx/index.d.ts +29 -0
  130. package/dist/safe-bash/commands/pptx/index.d.ts.map +1 -0
  131. package/dist/safe-bash/commands/pptx/index.js +153 -0
  132. package/dist/safe-bash/commands/pptx/index.js.map +1 -0
  133. package/dist/safe-bash/commands/pr/index.browser.js +8 -6
  134. package/dist/safe-bash/commands/predicates.js +1 -1
  135. package/dist/safe-bash/commands/python/execution.d.ts +3 -0
  136. package/dist/safe-bash/commands/python/execution.d.ts.map +1 -0
  137. package/dist/safe-bash/commands/python/execution.js +193 -0
  138. package/dist/safe-bash/commands/python/execution.js.map +1 -0
  139. package/dist/safe-bash/commands/python/index.browser.js +21 -0
  140. package/dist/safe-bash/commands/python/index.browser.js.map +7 -0
  141. package/dist/safe-bash/commands/python/index.d.ts +49 -0
  142. package/dist/safe-bash/commands/python/index.d.ts.map +1 -0
  143. package/dist/safe-bash/commands/python/index.js +317 -0
  144. package/dist/safe-bash/commands/python/index.js.map +1 -0
  145. package/dist/safe-bash/commands/python/installation.d.ts +9 -0
  146. package/dist/safe-bash/commands/python/installation.d.ts.map +1 -0
  147. package/dist/safe-bash/commands/python/installation.js +80 -0
  148. package/dist/safe-bash/commands/python/installation.js.map +1 -0
  149. package/dist/safe-bash/commands/python/invocation.d.ts +10 -0
  150. package/dist/safe-bash/commands/python/invocation.d.ts.map +1 -0
  151. package/dist/safe-bash/commands/python/invocation.js +81 -0
  152. package/dist/safe-bash/commands/python/invocation.js.map +1 -0
  153. package/dist/safe-bash/commands/python/node.d.ts +12 -0
  154. package/dist/safe-bash/commands/python/node.d.ts.map +1 -0
  155. package/dist/safe-bash/commands/python/node.js +33 -0
  156. package/dist/safe-bash/commands/python/node.js.map +1 -0
  157. package/dist/safe-bash/commands/python/provisioning-runtime.d.ts +5 -0
  158. package/dist/safe-bash/commands/python/provisioning-runtime.d.ts.map +1 -0
  159. package/dist/safe-bash/commands/python/provisioning-runtime.js +179 -0
  160. package/dist/safe-bash/commands/python/provisioning-runtime.js.map +1 -0
  161. package/dist/safe-bash/commands/python/provisioning.d.ts +49 -0
  162. package/dist/safe-bash/commands/python/provisioning.d.ts.map +1 -0
  163. package/dist/safe-bash/commands/python/provisioning.js +320 -0
  164. package/dist/safe-bash/commands/python/provisioning.js.map +1 -0
  165. package/dist/safe-bash/commands/python/reply.d.ts +3 -0
  166. package/dist/safe-bash/commands/python/reply.d.ts.map +1 -0
  167. package/dist/safe-bash/commands/python/reply.js +103 -0
  168. package/dist/safe-bash/commands/python/reply.js.map +1 -0
  169. package/dist/safe-bash/commands/python/worker.browser.js +736 -0
  170. package/dist/safe-bash/commands/python/worker.browser.js.map +7 -0
  171. package/dist/safe-bash/commands/python/worker.d.ts +47 -0
  172. package/dist/safe-bash/commands/python/worker.d.ts.map +1 -0
  173. package/dist/safe-bash/commands/python/worker.js +373 -0
  174. package/dist/safe-bash/commands/python/worker.js.map +1 -0
  175. package/dist/safe-bash/commands/safejs/index.d.ts +5 -17
  176. package/dist/safe-bash/commands/safejs/index.d.ts.map +1 -1
  177. package/dist/safe-bash/commands/safejs/index.js +3 -202
  178. package/dist/safe-bash/commands/safejs/index.js.map +1 -1
  179. package/dist/safe-bash/commands/safejs/runtime.d.ts +15 -0
  180. package/dist/safe-bash/commands/safejs/runtime.d.ts.map +1 -0
  181. package/dist/safe-bash/commands/safejs/runtime.js +190 -0
  182. package/dist/safe-bash/commands/safejs/runtime.js.map +1 -0
  183. package/dist/safe-bash/commands/tsort/index.browser.js +8 -6
  184. package/dist/safe-bash/commands/xml/index.browser.js +9 -7
  185. package/dist/safe-bash/commands/yq/index.browser.js +10 -8
  186. package/dist/safe-bash/commands/yq/index.d.ts.map +1 -1
  187. package/dist/safe-bash/commands/yq/index.js +32 -4
  188. package/dist/safe-bash/commands/yq/index.js.map +1 -1
  189. package/dist/safe-bash/commands/yq/parser.d.ts.map +1 -1
  190. package/dist/safe-bash/commands/yq/parser.js +21 -5
  191. package/dist/safe-bash/commands/yq/parser.js.map +1 -1
  192. package/dist/safe-bash/contracts/command.d.ts +2 -0
  193. package/dist/safe-bash/contracts/command.d.ts.map +1 -1
  194. package/dist/safe-bash/contracts/command.js +6 -2
  195. package/dist/safe-bash/contracts/command.js.map +1 -1
  196. package/dist/safe-bash/contracts/filesystem-descriptor.d.ts +13 -0
  197. package/dist/safe-bash/contracts/filesystem-descriptor.d.ts.map +1 -0
  198. package/dist/safe-bash/contracts/filesystem-descriptor.js +189 -0
  199. package/dist/safe-bash/contracts/filesystem-descriptor.js.map +1 -0
  200. package/dist/safe-bash/contracts/filesystem-output-budget.d.ts +15 -0
  201. package/dist/safe-bash/contracts/filesystem-output-budget.d.ts.map +1 -0
  202. package/dist/safe-bash/contracts/filesystem-output-budget.js +69 -0
  203. package/dist/safe-bash/contracts/filesystem-output-budget.js.map +1 -0
  204. package/dist/safe-bash/contracts/filesystem-output.d.ts +11 -5
  205. package/dist/safe-bash/contracts/filesystem-output.d.ts.map +1 -1
  206. package/dist/safe-bash/contracts/filesystem-output.js +229 -20
  207. package/dist/safe-bash/contracts/filesystem-output.js.map +1 -1
  208. package/dist/safe-bash/contracts/io.d.ts +29 -0
  209. package/dist/safe-bash/contracts/io.d.ts.map +1 -1
  210. package/dist/safe-bash/contracts/io.js +317 -37
  211. package/dist/safe-bash/contracts/io.js.map +1 -1
  212. package/dist/safe-bash/core.browser.js +5963 -6522
  213. package/dist/safe-bash/core.browser.js.map +4 -4
  214. package/dist/safe-bash/core.d.ts +2 -0
  215. package/dist/safe-bash/core.d.ts.map +1 -1
  216. package/dist/safe-bash/core.js +2 -0
  217. package/dist/safe-bash/core.js.map +1 -1
  218. package/dist/safe-bash/index.d.ts +1 -0
  219. package/dist/safe-bash/index.d.ts.map +1 -1
  220. package/dist/safe-bash/index.js +1 -0
  221. package/dist/safe-bash/index.js.map +1 -1
  222. package/dist/safe-bash/opt-in/commands/cmp/compare.js +95 -0
  223. package/dist/safe-bash/opt-in/commands/cmp/index.d.ts +6 -0
  224. package/dist/safe-bash/opt-in/commands/cmp/index.js +99 -0
  225. package/dist/safe-bash/opt-in/commands/cmp/io.js +218 -0
  226. package/dist/safe-bash/opt-in/commands/cmp/options.d.ts +24 -0
  227. package/dist/safe-bash/opt-in/commands/cmp/options.js +195 -0
  228. package/dist/safe-bash/opt-in/commands/dd/conversions.js +152 -0
  229. package/dist/safe-bash/opt-in/commands/dd/index.d.ts +18 -0
  230. package/dist/safe-bash/opt-in/commands/dd/index.js +561 -0
  231. package/dist/safe-bash/opt-in/commands/dd/io.d.ts +31 -0
  232. package/dist/safe-bash/opt-in/commands/dd/io.js +244 -0
  233. package/dist/safe-bash/opt-in/commands/dd/options.js +184 -0
  234. package/dist/safe-bash/opt-in/commands/dd/report.js +69 -0
  235. package/dist/safe-bash/opt-in/commands/install/arguments.js +224 -0
  236. package/dist/safe-bash/opt-in/commands/install/index.d.ts +6 -0
  237. package/dist/safe-bash/opt-in/commands/install/index.js +678 -0
  238. package/dist/safe-bash/opt-in/commands/install/mode.js +71 -0
  239. package/dist/safe-bash/opt-in/commands/install/options.d.ts +36 -0
  240. package/dist/safe-bash/opt-in/commands/install/options.js +27 -0
  241. package/dist/safe-bash/opt-in/commands/shuf/args.js +179 -0
  242. package/dist/safe-bash/opt-in/commands/shuf/index.d.ts +6 -0
  243. package/dist/safe-bash/opt-in/commands/shuf/index.js +20 -0
  244. package/dist/safe-bash/opt-in/commands/shuf/input.js +93 -0
  245. package/dist/safe-bash/opt-in/commands/shuf/options.d.ts +9 -0
  246. package/dist/safe-bash/opt-in/commands/shuf/options.js +8 -0
  247. package/dist/safe-bash/opt-in/commands/shuf/random.js +114 -0
  248. package/dist/safe-bash/opt-in/commands/shuf/shuf.d.ts +3 -0
  249. package/dist/safe-bash/opt-in/commands/shuf/shuf.js +230 -0
  250. package/dist/safe-bash/opt-in/commands/shuf/usage.js +24 -0
  251. package/dist/safe-bash/opt-in/commands/truncate/arguments.js +186 -0
  252. package/dist/safe-bash/opt-in/commands/truncate/index.d.ts +9 -0
  253. package/dist/safe-bash/opt-in/commands/truncate/index.js +244 -0
  254. package/dist/safe-bash/opt-in/commands/yes/index.d.ts +11 -0
  255. package/dist/safe-bash/opt-in/commands/yes/index.js +167 -0
  256. package/dist/safe-bash/opt-in/commands/yq/arguments.js +177 -0
  257. package/dist/safe-bash/opt-in/commands/yq/evaluate.js +564 -0
  258. package/dist/safe-bash/opt-in/commands/yq/expression.js +183 -0
  259. package/dist/safe-bash/opt-in/commands/yq/inplace.js +65 -0
  260. package/dist/safe-bash/opt-in/commands/yq/mike.d.ts +9 -0
  261. package/dist/safe-bash/opt-in/commands/yq/mike.js +192 -0
  262. package/dist/safe-bash/opt-in/commands/yq/native-encoder.js +165 -0
  263. package/dist/safe-bash/opt-in/commands/yq/native-work.d.ts +44 -0
  264. package/dist/safe-bash/opt-in/commands/yq/native-work.js +188 -0
  265. package/dist/safe-bash/opt-in/commands/yq/nodes.js +621 -0
  266. package/dist/safe-bash/opt-in/entrypoints/arrays.d.ts +1 -0
  267. package/dist/safe-bash/opt-in/entrypoints/arrays.js +1 -0
  268. package/dist/safe-bash/opt-in/entrypoints/cmp.d.ts +2 -0
  269. package/dist/safe-bash/opt-in/entrypoints/cmp.js +1 -0
  270. package/dist/safe-bash/opt-in/entrypoints/dd.d.ts +2 -0
  271. package/dist/safe-bash/opt-in/entrypoints/dd.js +1 -0
  272. package/dist/safe-bash/opt-in/entrypoints/devices.d.ts +2 -0
  273. package/dist/safe-bash/opt-in/entrypoints/devices.js +1 -0
  274. package/dist/safe-bash/opt-in/entrypoints/install.d.ts +2 -0
  275. package/dist/safe-bash/opt-in/entrypoints/install.js +1 -0
  276. package/dist/safe-bash/opt-in/entrypoints/jobs.d.ts +1 -0
  277. package/dist/safe-bash/opt-in/entrypoints/jobs.js +1 -0
  278. package/dist/safe-bash/opt-in/entrypoints/mapfile.d.ts +1 -0
  279. package/dist/safe-bash/opt-in/entrypoints/mapfile.js +1 -0
  280. package/dist/safe-bash/opt-in/entrypoints/read.d.ts +2 -0
  281. package/dist/safe-bash/opt-in/entrypoints/read.js +1 -0
  282. package/dist/safe-bash/opt-in/entrypoints/shuf.d.ts +2 -0
  283. package/dist/safe-bash/opt-in/entrypoints/shuf.js +1 -0
  284. package/dist/safe-bash/opt-in/entrypoints/trap.d.ts +2 -0
  285. package/dist/safe-bash/opt-in/entrypoints/trap.js +1 -0
  286. package/dist/safe-bash/opt-in/entrypoints/truncate.d.ts +2 -0
  287. package/dist/safe-bash/opt-in/entrypoints/truncate.js +1 -0
  288. package/dist/safe-bash/opt-in/entrypoints/yes.d.ts +2 -0
  289. package/dist/safe-bash/opt-in/entrypoints/yes.js +1 -0
  290. package/dist/safe-bash/opt-in/entrypoints/yq.d.ts +3 -0
  291. package/dist/safe-bash/opt-in/entrypoints/yq.js +1 -0
  292. package/dist/safe-bash/opt-in/fs/devices/index.d.ts +8 -0
  293. package/dist/safe-bash/opt-in/fs/devices/index.js +292 -0
  294. package/dist/safe-bash/opt-in/optional.d.ts +28 -0
  295. package/dist/safe-bash/opt-in/optional.js +14 -0
  296. package/dist/safe-bash/opt-in/shell/extensions/arrays/index.d.ts +2 -0
  297. package/dist/safe-bash/opt-in/shell/extensions/arrays/index.js +4 -0
  298. package/dist/safe-bash/opt-in/shell/extensions/jobs/index.d.ts +2 -0
  299. package/dist/safe-bash/opt-in/shell/extensions/jobs/index.js +291 -0
  300. package/dist/safe-bash/opt-in/shell/extensions/jobs/state.js +374 -0
  301. package/dist/safe-bash/opt-in/shell/extensions/mapfile/index.d.ts +24 -0
  302. package/dist/safe-bash/opt-in/shell/extensions/mapfile/index.js +338 -0
  303. package/dist/safe-bash/opt-in/shell/extensions/read/index.d.ts +5 -0
  304. package/dist/safe-bash/opt-in/shell/extensions/read/index.js +351 -0
  305. package/dist/safe-bash/opt-in/shell/extensions/trap/index.d.ts +9 -0
  306. package/dist/safe-bash/opt-in/shell/extensions/trap/index.js +210 -0
  307. package/dist/safe-bash/optional-host.d.ts +5 -0
  308. package/dist/safe-bash/optional-host.d.ts.map +1 -0
  309. package/dist/safe-bash/optional-host.js +3 -0
  310. package/dist/safe-bash/optional-host.js.map +1 -0
  311. package/dist/safe-bash/shell/arrays/bindings.d.ts +15 -5
  312. package/dist/safe-bash/shell/arrays/bindings.d.ts.map +1 -1
  313. package/dist/safe-bash/shell/arrays/bindings.js +131 -23
  314. package/dist/safe-bash/shell/arrays/bindings.js.map +1 -1
  315. package/dist/safe-bash/shell/arrays/ledger.d.ts +1 -0
  316. package/dist/safe-bash/shell/arrays/ledger.d.ts.map +1 -1
  317. package/dist/safe-bash/shell/arrays/ledger.js +19 -0
  318. package/dist/safe-bash/shell/arrays/ledger.js.map +1 -1
  319. package/dist/safe-bash/shell/arrays/state.d.ts.map +1 -1
  320. package/dist/safe-bash/shell/arrays/state.js +3 -1
  321. package/dist/safe-bash/shell/arrays/state.js.map +1 -1
  322. package/dist/safe-bash/shell/arrays/syntax.d.ts +4 -1
  323. package/dist/safe-bash/shell/arrays/syntax.d.ts.map +1 -1
  324. package/dist/safe-bash/shell/arrays/syntax.js +3 -3
  325. package/dist/safe-bash/shell/arrays/syntax.js.map +1 -1
  326. package/dist/safe-bash/shell/conditional.js +1 -1
  327. package/dist/safe-bash/shell/descriptors.d.ts +31 -0
  328. package/dist/safe-bash/shell/descriptors.d.ts.map +1 -0
  329. package/dist/safe-bash/shell/descriptors.js +234 -0
  330. package/dist/safe-bash/shell/descriptors.js.map +1 -0
  331. package/dist/safe-bash/shell/diagnostic-name-ranges.d.ts +2 -0
  332. package/dist/safe-bash/shell/diagnostic-name-ranges.d.ts.map +1 -0
  333. package/dist/safe-bash/shell/diagnostic-name-ranges.js +717 -0
  334. package/dist/safe-bash/shell/diagnostic-name-ranges.js.map +1 -0
  335. package/dist/safe-bash/shell/diagnostic-name.d.ts +3 -0
  336. package/dist/safe-bash/shell/diagnostic-name.d.ts.map +1 -0
  337. package/dist/safe-bash/shell/diagnostic-name.js +89 -0
  338. package/dist/safe-bash/shell/diagnostic-name.js.map +1 -0
  339. package/dist/safe-bash/shell/display.js +1 -1
  340. package/dist/safe-bash/shell/display.js.map +1 -1
  341. package/dist/safe-bash/shell/extensions.d.ts +188 -0
  342. package/dist/safe-bash/shell/extensions.d.ts.map +1 -0
  343. package/dist/safe-bash/shell/extensions.js +141 -0
  344. package/dist/safe-bash/shell/extensions.js.map +1 -0
  345. package/dist/safe-bash/shell/input.d.ts +66 -14
  346. package/dist/safe-bash/shell/input.d.ts.map +1 -1
  347. package/dist/safe-bash/shell/input.js +947 -173
  348. package/dist/safe-bash/shell/input.js.map +1 -1
  349. package/dist/safe-bash/shell/parser.d.ts +38 -6
  350. package/dist/safe-bash/shell/parser.d.ts.map +1 -1
  351. package/dist/safe-bash/shell/parser.js +339 -52
  352. package/dist/safe-bash/shell/parser.js.map +1 -1
  353. package/dist/safe-bash/shell/runtime.d.ts +92 -20
  354. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  355. package/dist/safe-bash/shell/runtime.js +2547 -689
  356. package/dist/safe-bash/shell/runtime.js.map +1 -1
  357. package/dist/safe-bash/shell/shell.d.ts.map +1 -1
  358. package/dist/safe-bash/shell/shell.js +52 -21
  359. package/dist/safe-bash/shell/shell.js.map +1 -1
  360. package/dist/safe-bash/shell/types.d.ts +4 -0
  361. package/dist/safe-bash/shell/types.d.ts.map +1 -1
  362. package/dist/safe-bash/shell/types.js.map +1 -1
  363. package/package.json +99 -5
  364. package/dist/safe-bash/chunks/chunk-4DBWBEU7.js +0 -194
  365. package/dist/safe-bash/chunks/chunk-4DBWBEU7.js.map +0 -7
  366. package/dist/safe-bash/chunks/chunk-CXMIIXJA.js +0 -178
  367. package/dist/safe-bash/chunks/chunk-CXMIIXJA.js.map +0 -7
  368. package/dist/safe-bash/chunks/chunk-HHJSJWUD.js.map +0 -7
  369. /package/dist/safe-bash/chunks/{bz2-B2RXT4EX.js.map → bz2-ACU4EOVE.js.map} +0 -0
  370. /package/dist/safe-bash/chunks/{chunk-RNKTWFEM.js.map → chunk-CDBQXRWL.js.map} +0 -0
  371. /package/dist/safe-bash/chunks/{chunk-AFH57PPL.js.map → chunk-IEKESMO4.js.map} +0 -0
  372. /package/dist/safe-bash/chunks/{chunk-DPJWEGIX.js.map → chunk-JLBIUYT3.js.map} +0 -0
  373. /package/dist/safe-bash/chunks/{chunk-PVJJFCGA.js.map → chunk-M3GPCYSP.js.map} +0 -0
  374. /package/dist/safe-bash/chunks/{chunk-2KL6W6NB.js.map → chunk-O2EVBRKF.js.map} +0 -0
  375. /package/dist/safe-bash/chunks/{chunk-C7QNJ3D4.js.map → chunk-WYTI64IU.js.map} +0 -0
  376. /package/dist/safe-bash/chunks/{xz-AVXFDXW4.js.map → xz-U4QDTPZL.js.map} +0 -0
  377. /package/dist/safe-bash/chunks/{zstd-R6RPHJYO.js.map → zstd-OG5RPYXO.js.map} +0 -0
@@ -1,16 +1,17 @@
1
- import { BackgroundJobs, BackgroundResources, backgroundResources, backgroundPrune, backgroundDestination, forwardBackgroundDestination } from "./background-jobs.js";
2
1
  import { PublicDiagnostic, publicDiagnosticMessage } from "../diagnostics.js";
3
2
  import { writeDiagnostic } from "../escaping.js";
4
3
  import { cancelTurn, monotonicNow, registerYieldCheckpoint, scheduleTurn, yieldTurn } from "../contracts/yield.js";
5
- import { ACCESS_MODES, FsError, composeMiddleware, createBytePipe, pipeBytes, resolvePath, toByteSource, validateExitCode, writeText, } from "../contracts/index.js";
4
+ import { ACCESS_MODES, FsError, composeMiddleware, createBytePipe, pipeBytes, resolvePath, validateExitCode, writeText, } from "../contracts/index.js";
6
5
  import { concatShellValues, shellValueByteLength, shellValueBytes, shellValueFromBytes, shellValueText } from "../contracts/value.js";
7
6
  import { createCommandArguments, getCommandArguments } from "../contracts/command.js";
8
7
  import { ValueArena } from "./value-state.js";
9
- import { parseArraySubscript, compoundEntryWords, HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
10
- import { SourceLineIndex } from "./source-line-index.js";
8
+ import { parseArraySubscript, compoundEntryWords, HereDocumentSyntaxError, functionReprintedLines, hereDocumentWords, parseCompoundArrayValue, parseShellInputUnit, parseShellUnit } from "./parser.js";
11
9
  import { ShellLimitError, ShellSyntaxError } from "./types.js";
10
+ import { forkExtensions } from "./extensions.js";
11
+ import { prepareBytesInput, prepareFileInput, ShellInput } from "./input.js";
12
+ import { observeDescriptor, PipeDescriptorFrame, pipeObservation } from "./descriptors.js";
13
+ import { SourceLineIndex } from "./source-line-index.js";
12
14
  import { scopeFileSystem } from "@poe-platform/safe-fs/core";
13
- import { fileInput, ShellInput } from "./input.js";
14
15
  import { evaluateArithmetic, prepareArithmetic } from "./arithmetic.js";
15
16
  import { defaultMaxParseUnits, ParseBudget } from "./parse-budget.js";
16
17
  import { BraceExpansionFailure, expandBraces } from "./brace-expansion.js";
@@ -19,11 +20,12 @@ import { compilePattern, compilePatternBoundaries, matchesPattern } from "./patt
19
20
  import { nextCodePointOffset, scanString, stringCheckpoint } from "./string-operations.js";
20
21
  import { selectMenu } from "./select-menu.js";
21
22
  import { byteLocale } from "./locale.js";
23
+ import { diagnosticCommandName } from "./diagnostic-name.js";
22
24
  import { trimParameter } from "./parameter-trim.js";
23
25
  import { ownedShellSource } from "./source-value.js";
24
26
  import { functionDisplay } from "./display.js";
25
27
  import { ConditionalUnsupported, evaluateConditional } from "./conditional.js";
26
- import { invocationScope, throwCleanupFailures } from "./cleanup.js";
28
+ import { invocationScope, throwCleanupFailures, InvocationScope } from "./cleanup.js";
27
29
  import { bindFileOutputBudget, openFileOutput } from "../contracts/filesystem-output.js";
28
30
  import { outputFailure } from "../contracts/io.js";
29
31
  import { executionCommands } from "../commands/execution.js";
@@ -63,9 +65,10 @@ export const defaultLimits = {
63
65
  };
64
66
  const shellBuiltinNames = new Set([
65
67
  ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "declare", "mapfile", "readarray",
66
- "wait", "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
68
+ "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
67
69
  ]);
68
70
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
71
+ const extensionExitFailures = new WeakMap();
69
72
  const specialBuiltinNames = new Set([":", ".", "break", "continue", "eval", "exit", "export", "readonly", "return", "set", "shift", "unset"]);
70
73
  const zeroPositionKey = "-1";
71
74
  const unsupportedSetOptionNames = new Set([
@@ -73,6 +76,20 @@ const unsupportedSetOptionNames = new Set([
73
76
  "ignoreeof", "interactive-comments", "keyword", "monitor", "noclobber", "noexec", "noglob", "nolog",
74
77
  "notify", "onecmd", "physical", "posix", "privileged", "verbose", "vi", "xtrace",
75
78
  ]);
79
+ function commandSpelling(command) {
80
+ if (command.kind === "arithmetic")
81
+ return `((${command.source}))`;
82
+ if (command.kind === "conditional")
83
+ return `[[${command.source}]]`;
84
+ const redirects = command.redirects.map(redirect => {
85
+ const target = redirect.target.spelling ?? redirect.target.plain ?? "";
86
+ if (redirect.operator === ">&" || redirect.operator === "<&")
87
+ return `${redirect.descriptor}${redirect.operator}${target}`;
88
+ const implicit = redirect.operator.startsWith("<") ? 0 : 1;
89
+ return `${redirect.descriptor === implicit ? "" : redirect.descriptor}${redirect.operator} ${target}`;
90
+ });
91
+ return [...command.words.map(word => word.spelling ?? word.plain ?? ""), ...redirects].join(" ");
92
+ }
76
93
  export function resolveLimits(...limits) {
77
94
  const result = Object.assign({}, defaultLimits, ...limits);
78
95
  for (const [key, value] of Object.entries(result)) {
@@ -82,8 +99,6 @@ export function resolveLimits(...limits) {
82
99
  }
83
100
  return result;
84
101
  }
85
- const runtimeFileSystems = new WeakMap();
86
- const inputScopes = new WeakMap();
87
102
  const budgetedSinks = new WeakMap();
88
103
  async function sortExpansionStrings(values, work) {
89
104
  const compare = async (left, right) => {
@@ -119,6 +134,75 @@ async function sortExpansionStrings(values, work) {
119
134
  await sift(0, end);
120
135
  }
121
136
  }
137
+ class ExecutionCleanup {
138
+ budget;
139
+ controller = new AbortController();
140
+ failures = [];
141
+ #pending = [];
142
+ #closed = false;
143
+ #drain;
144
+ #failure;
145
+ constructor(budget) {
146
+ this.budget = budget;
147
+ }
148
+ register(cleanup) {
149
+ this.budget.signal.throwIfAborted();
150
+ if (this.#failure)
151
+ throw this.#failure.reason;
152
+ if (this.#closed)
153
+ throw new TypeError("Execution cleanup enrollment is closed");
154
+ if (typeof cleanup !== "function")
155
+ throw new TypeError("Execution cleanup must be callable");
156
+ this.budget.cpuCheckpoint();
157
+ const allocation = this.budget.values.scope();
158
+ try {
159
+ allocation.reserve(96, 1);
160
+ this.#pending.push({ cleanup, allocation });
161
+ }
162
+ catch (reason) {
163
+ allocation.close();
164
+ throw reason;
165
+ }
166
+ }
167
+ abort(reason) {
168
+ this.#failure ??= { reason };
169
+ if (this.#pending.length || this.#drain)
170
+ this.controller.abort(reason);
171
+ }
172
+ drain() {
173
+ return this.#drain ??= Promise.resolve().then(async () => {
174
+ const retained = [];
175
+ try {
176
+ while (this.#pending.length) {
177
+ const pending = this.#pending.splice(0);
178
+ const results = await Promise.allSettled(pending.map(async (entry) => {
179
+ let failed = false;
180
+ try {
181
+ await entry.cleanup();
182
+ }
183
+ catch (reason) {
184
+ failed = true;
185
+ retained.push(entry.allocation);
186
+ throw reason;
187
+ }
188
+ finally {
189
+ if (!failed)
190
+ entry.allocation.close();
191
+ }
192
+ }));
193
+ for (const result of results)
194
+ if (result.status === "rejected")
195
+ this.failures.push(result.reason);
196
+ }
197
+ }
198
+ finally {
199
+ this.#closed = true;
200
+ for (const allocation of retained)
201
+ allocation.close();
202
+ }
203
+ });
204
+ }
205
+ }
122
206
  export class Budget {
123
207
  limits;
124
208
  onInternalError;
@@ -126,6 +210,7 @@ export class Budget {
126
210
  pathLookup = new PathLookup();
127
211
  parsing;
128
212
  values;
213
+ executionCleanup = new ExecutionCleanup(this);
129
214
  commands = 0;
130
215
  iterations = 0;
131
216
  bytes = 0;
@@ -212,6 +297,27 @@ export class Budget {
212
297
  this.fail("maxSourceBytes");
213
298
  this.sourceBytes += bytes;
214
299
  }
300
+ async writeCounted(chunk, write, signal = this.signal) {
301
+ signal.throwIfAborted();
302
+ if (!(chunk instanceof Uint8Array))
303
+ throw new TypeError("Shell output must be Uint8Array");
304
+ const reserved = chunk.byteLength;
305
+ if (reserved > this.limits.maxOutputBytes - this.bytes)
306
+ this.fail("maxOutputBytes");
307
+ this.bytes += reserved;
308
+ try {
309
+ const count = await write();
310
+ if (!Number.isSafeInteger(count) || count < 0 || count > reserved)
311
+ throw new FsError("EIO", { syscall: "write", message: "invalid byte count" });
312
+ this.bytes -= reserved - count;
313
+ signal.throwIfAborted();
314
+ return count;
315
+ }
316
+ catch (error) {
317
+ signal.throwIfAborted();
318
+ throw error;
319
+ }
320
+ }
215
321
  sink(sink, signal = this.signal) {
216
322
  const ownership = budgetedSinks.get(sink);
217
323
  if (ownership?.budget === this && ownership.write === sink.write)
@@ -252,7 +358,6 @@ export class Budget {
252
358
  await interruptible(sink.write(chunk), signal);
253
359
  },
254
360
  };
255
- forwardBackgroundDestination(output, sink);
256
361
  budgetedSinks.set(output, { budget: this, write: output.write, ...(ownership?.write === sink.write && ownership.file ? { file: ownership.file } : {}) });
257
362
  return output;
258
363
  }
@@ -319,8 +424,138 @@ export class Capture {
319
424
  const typedSavedVariables = new WeakMap();
320
425
  const valueScope = Symbol("shell value allocation scope");
321
426
  const invokedValues = new WeakMap();
322
- function isolateIO(io) {
323
- return { ...io, nameExpansionContext: undefined, ...(io.descriptors ? { descriptors: new Map([...io.descriptors].map(([number, descriptor]) => [number, { ...descriptor }])) } : {}) };
427
+ const functionDiagnostics = new WeakMap();
428
+ const childIdentities = new WeakMap();
429
+ class DescriptorLifetime {
430
+ finalize;
431
+ #references = 1;
432
+ #ownerReleased = false;
433
+ #closing;
434
+ constructor(finalize) {
435
+ this.finalize = finalize;
436
+ }
437
+ acquire() {
438
+ if (!this.#references)
439
+ throw new FsError("EBADF");
440
+ this.#references++;
441
+ let released = false;
442
+ return () => {
443
+ if (released)
444
+ return this.#closing ?? Promise.resolve();
445
+ released = true;
446
+ return this.#release();
447
+ };
448
+ }
449
+ release() {
450
+ if (this.#ownerReleased)
451
+ return this.#closing ?? Promise.resolve();
452
+ this.#ownerReleased = true;
453
+ return this.#release();
454
+ }
455
+ #release() {
456
+ if (--this.#references)
457
+ return Promise.resolve();
458
+ return this.#closing ??= Promise.resolve().then(this.finalize);
459
+ }
460
+ }
461
+ class PreparedDescriptorFrame {
462
+ references;
463
+ budget;
464
+ #bindings = new Map();
465
+ #work = Promise.resolve();
466
+ #closing;
467
+ #retireCleanup;
468
+ constructor(references, budget) {
469
+ this.references = references;
470
+ this.budget = budget;
471
+ this.#retireCleanup = references.scope.register(() => this.close());
472
+ }
473
+ acquire(descriptors) {
474
+ if (this.#closing)
475
+ throw new FsError("EBADF");
476
+ for (const [number, descriptor] of descriptors) {
477
+ if (descriptor.closed || !descriptor.lifetime)
478
+ continue;
479
+ const allocation = this.budget.values.scope();
480
+ try {
481
+ allocation.reserve(64, 1);
482
+ const release = descriptor.lifetime.acquire();
483
+ this.#bindings.set(number, { lifetime: descriptor.lifetime, async release() {
484
+ try {
485
+ await release();
486
+ }
487
+ finally {
488
+ allocation.close();
489
+ }
490
+ } });
491
+ }
492
+ catch (reason) {
493
+ allocation.close();
494
+ throw reason;
495
+ }
496
+ }
497
+ }
498
+ reconcile(descriptors) {
499
+ if (this.#closing)
500
+ return Promise.reject(new FsError("EBADF"));
501
+ const previous = this.#bindings;
502
+ this.#bindings = new Map();
503
+ const failures = [];
504
+ const inherited = new Set([...previous.values()].map(binding => binding.lifetime));
505
+ const surviving = new Map([...descriptors].filter(([, descriptor]) => descriptor.lifetime && inherited.has(descriptor.lifetime)));
506
+ try {
507
+ this.acquire(surviving);
508
+ }
509
+ catch (reason) {
510
+ failures.push(reason);
511
+ }
512
+ const activePipes = new Set([...descriptors.values()].filter(descriptor => !descriptor.closed).map(descriptor => descriptor.pipe));
513
+ this.#work = this.#work.then(async () => {
514
+ const retired = await Promise.allSettled([
515
+ ...[...previous.values()].map(binding => binding.release()),
516
+ ...[...this.references.references].filter(reference => !activePipes.has(reference)).map(reference => reference.close()),
517
+ ]);
518
+ failures.push(...retired.filter(result => result.status === "rejected").map(result => result.reason));
519
+ throwCleanupFailures(failures);
520
+ });
521
+ return this.#work;
522
+ }
523
+ close() {
524
+ return this.#closing ??= Promise.resolve().then(async () => {
525
+ const failures = [];
526
+ try {
527
+ await this.#work;
528
+ }
529
+ catch (reason) {
530
+ failures.push(reason);
531
+ }
532
+ const retired = await Promise.allSettled([...this.#bindings.values()].map(binding => binding.release()));
533
+ this.#bindings.clear();
534
+ failures.push(...retired.filter(result => result.status === "rejected").map(result => result.reason));
535
+ throwCleanupFailures(failures);
536
+ this.#retireCleanup();
537
+ });
538
+ }
539
+ }
540
+ function isolateIO(io, references) {
541
+ const descriptors = new Map();
542
+ for (const [number, descriptor] of io.descriptors ?? []) {
543
+ if (number === 0 && descriptor.input !== io.stdin || number === 1 && descriptor.output !== io.stdout || number === 2 && descriptor.output !== io.stderr)
544
+ continue;
545
+ if (descriptor.closed || !descriptor.pipe)
546
+ descriptors.set(number, { ...descriptor });
547
+ else {
548
+ const pipe = references.acquire(descriptor.pipe);
549
+ descriptors.set(number, { ...descriptor, pipe });
550
+ }
551
+ }
552
+ if (!descriptors.has(0))
553
+ descriptors.set(0, { input: io.stdin, ...(io.stdinIsDefault === undefined ? {} : { stdinIsDefault: io.stdinIsDefault }) });
554
+ if (!descriptors.has(1))
555
+ descriptors.set(1, { output: io.stdout });
556
+ if (!descriptors.has(2))
557
+ descriptors.set(2, { output: io.stderr });
558
+ return { ...io, nameExpansionContext: undefined, descriptors, ...(io.assignmentDiagnosticContext === undefined ? {} : { assignmentDiagnosticContext: { ...io.assignmentDiagnosticContext } }) };
324
559
  }
325
560
  function activeIO(io) {
326
561
  const input = io.descriptors?.get(0);
@@ -349,16 +584,20 @@ class Flow extends Error {
349
584
  kind;
350
585
  status;
351
586
  levels;
352
- constructor(kind, status, levels = 1) {
587
+ previousStatus;
588
+ expansionFailure;
589
+ constructor(kind, status, levels = 1, previousStatus, expansionFailure = false) {
353
590
  super(kind);
354
591
  this.kind = kind;
355
592
  this.status = status;
356
593
  this.levels = levels;
594
+ this.previousStatus = previousStatus;
595
+ this.expansionFailure = expansionFailure;
357
596
  }
358
597
  }
359
598
  const completedFlows = new WeakSet();
360
- function completedExit(status, kind = "exit", levels = 1) {
361
- const flow = new Flow(kind, status, levels);
599
+ function completedExit(status, kind = "exit", levels = 1, previousStatus, expansionFailure = false) {
600
+ const flow = new Flow(kind, status, levels, previousStatus, expansionFailure);
362
601
  completedFlows.add(flow);
363
602
  return flow;
364
603
  }
@@ -389,6 +628,15 @@ class CommandFailure extends Error {
389
628
  }
390
629
  class FatalCommandFailure extends CommandFailure {
391
630
  }
631
+ class DiscardCommandFailure extends CommandFailure {
632
+ variableName;
633
+ origin;
634
+ constructor(variableName, status, origin) {
635
+ super(`${variableName}: readonly variable`, status);
636
+ this.variableName = variableName;
637
+ this.origin = origin;
638
+ }
639
+ }
392
640
  class ParameterExpansionFailure extends ExpansionFailure {
393
641
  }
394
642
  class NounsetFailure extends ExpansionFailure {
@@ -400,6 +648,8 @@ class NounsetDiagnosticFailure extends Flow {
400
648
  this.reason = reason;
401
649
  }
402
650
  }
651
+ class ExtensionCheckpointFailure extends NounsetDiagnosticFailure {
652
+ }
403
653
  class PipelineClosed extends Error {
404
654
  code = "EPIPE";
405
655
  constructor() { super("Pipeline consumer exited"); }
@@ -429,7 +679,6 @@ function signalSink(sink, signal) {
429
679
  } } : {}),
430
680
  async write(chunk) { signal.throwIfAborted(); await interruptible(write(chunk), signal); },
431
681
  };
432
- forwardBackgroundDestination(output, sink);
433
682
  if (owned)
434
683
  budgetedSinks.set(output, { ...owned, write: output.write });
435
684
  return output;
@@ -446,7 +695,6 @@ function bindCommandIO(context) {
446
695
  async function cloneState(state, signal, scope, inheritLocals = true) {
447
696
  const destination = await snapshotState(state, () => ({
448
697
  ...state,
449
- ...(state.backgroundJobs ? { backgroundJobs: new BackgroundJobs(state.backgroundJobs.execution) } : {}),
450
698
  variables: Object.assign(Object.create(null), state.variables),
451
699
  exported: new Set(state.exported), functions: new Map(state.functions), positional: [...state.positional],
452
700
  readonlyVariables: new Set(state.readonlyVariables),
@@ -506,6 +754,20 @@ async function cloneState(state, signal, scope, inheritLocals = true) {
506
754
  function cloneGetoptsBinding(state) {
507
755
  return { cursor: state.getopts ? cloneGetoptsState(state.getopts.cursor) : createGetoptsState(), integer: state.getopts?.integer ?? false };
508
756
  }
757
+ function shellCharacterWidth(bytes, offset, byteCount) {
758
+ if (byteCount)
759
+ return 1;
760
+ const first = bytes[offset];
761
+ const length = first >= 0xc2 && first <= 0xdf ? 2 : first >= 0xe0 && first <= 0xef ? 3 : first >= 0xf0 && first <= 0xf4 ? 4 : 1;
762
+ if (offset + length > bytes.length)
763
+ return 1;
764
+ for (let index = 1; index < length; index++) {
765
+ const byte = bytes[offset + index];
766
+ if (byte < 0x80 || byte > 0xbf || index === 1 && (first === 0xe0 && byte < 0xa0 || first === 0xed && byte > 0x9f || first === 0xf0 && byte < 0x90 || first === 0xf4 && byte > 0x8f))
767
+ return 1;
768
+ }
769
+ return length;
770
+ }
509
771
  function saveVariable(state, name) {
510
772
  const monitor = stateMonitor(state);
511
773
  const value = monitor?.values.get(name, state.variables[name] ?? "");
@@ -1120,6 +1382,7 @@ class InvocationCancellationOwner {
1120
1382
  }
1121
1383
  }
1122
1384
  const mapfileCallbackStates = new WeakSet();
1385
+ const runtimeFileSystems = new WeakMap();
1123
1386
  export class Runtime {
1124
1387
  fs;
1125
1388
  commands;
@@ -1136,6 +1399,7 @@ export class Runtime {
1136
1399
  cancellationMaxDepth;
1137
1400
  outcomeFrame;
1138
1401
  inputProfile;
1402
+ sourceFs;
1139
1403
  constructor(fs, commands, middleware, budget, signal = budget.signal, fileWrites = new Map(), outputFiles = new Map(), commandSignal = signal, cancellation, cancellationState, cancellationOwner, cancellationDepth, cancellationMaxDepth, outcomeFrame = undefined, inputProfile = fs) {
1140
1404
  this.fs = fs;
1141
1405
  this.commands = commands;
@@ -1152,8 +1416,9 @@ export class Runtime {
1152
1416
  this.cancellationMaxDepth = cancellationMaxDepth;
1153
1417
  this.outcomeFrame = outcomeFrame;
1154
1418
  this.inputProfile = inputProfile;
1419
+ this.sourceFs = runtimeFileSystems.get(fs) ?? fs;
1155
1420
  this.fs = scopeFileSystem(fs, () => budget.fileSystemOperation(), signal, () => budget.fileSystemCleanupOperation());
1156
- runtimeFileSystems.set(this.fs, runtimeFileSystems.get(fs) ?? fs);
1421
+ runtimeFileSystems.set(this.fs, this.sourceFs);
1157
1422
  const checkpoint = () => budget.cpuCheckpoint();
1158
1423
  registerYieldCheckpoint(signal, checkpoint);
1159
1424
  registerYieldCheckpoint(commandSignal, checkpoint);
@@ -1399,8 +1664,18 @@ export class Runtime {
1399
1664
  if (this.outcomeFrame)
1400
1665
  this.outcomeFrame.report = undefined;
1401
1666
  }
1402
- diagnostic(io, text) {
1403
- return writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: line ${io.diagnosticLine ?? 1}: ${text}\n`);
1667
+ async diagnostic(io, text) {
1668
+ const prefix = `${io.scriptName ?? "shell"}: line ${io.diagnosticLine ?? 1}: `;
1669
+ if (typeof text === "string")
1670
+ return writeDiagnostic(io.stderr, `${prefix}${text}\n`, this.signal);
1671
+ const allocation = this.budget.values.scope();
1672
+ try {
1673
+ const value = concatShellValues([prefix, text, "\n"], allocation);
1674
+ await io.stderr.write(shellValueBytes(value, allocation));
1675
+ }
1676
+ finally {
1677
+ allocation.close();
1678
+ }
1404
1679
  }
1405
1680
  writeVariable(state, name, value, origin = "assignment") {
1406
1681
  if (arrayStore(state)?.get(name))
@@ -1665,6 +1940,13 @@ export class Runtime {
1665
1940
  }
1666
1941
  }
1667
1942
  }
1943
+ assertArrayWritable(state, name, origin = "assignment") {
1944
+ if (!state.readonlyVariables?.has(name))
1945
+ return;
1946
+ if (state.extensions?.syntax.indexedDeclarations?.includes("readonly"))
1947
+ throw new DiscardCommandFailure(name, 1, origin);
1948
+ throw new ArrayFailure("readonly binding");
1949
+ }
1668
1950
  async arrayZero(state, name, expand, append = false, freeze = false) {
1669
1951
  const store = requireArrays(state);
1670
1952
  const operation = ArrayOwner.create(store.owner.ledger, store.owner);
@@ -1682,8 +1964,7 @@ export class Runtime {
1682
1964
  }
1683
1965
  const expanded = await expand();
1684
1966
  this.signal.throwIfAborted();
1685
- if (state.readonlyVariables?.has(name))
1686
- throw new ArrayFailure("readonly binding");
1967
+ this.assertArrayWritable(state, name);
1687
1968
  if (!watch.valid())
1688
1969
  throw new ArrayFailure("stale binding");
1689
1970
  const current = store.get(name);
@@ -1691,10 +1972,10 @@ export class Runtime {
1691
1972
  throw new ArrayFailure("stale binding");
1692
1973
  staged = await current.copy(this.signal);
1693
1974
  const index = staged.associative ? (await staged.keyIndex("0", operation, this.signal, true)) : 0;
1694
- const previous = current.values.get(index)?.text.rawValue ?? current.get(index) ?? "";
1975
+ const previous = current.getValue(index) ?? "";
1695
1976
  const value = !append ? expanded : typeof previous === "string" && typeof expanded === "string"
1696
1977
  ? await this.arrayJoin(operation, [previous, expanded], "") : concatShellValues([previous, expanded], valueAllocation);
1697
- const token = await valueToken(staged.owner, value, this.signal);
1978
+ const token = await textToken(staged.owner, value, this.signal);
1698
1979
  try {
1699
1980
  staged.insert(index, token);
1700
1981
  }
@@ -1704,8 +1985,7 @@ export class Runtime {
1704
1985
  }
1705
1986
  const supersede = await stateMonitor(state).prepareTypedPublication(name, operation, this.signal);
1706
1987
  this.signal.throwIfAborted();
1707
- if (state.readonlyVariables?.has(name))
1708
- throw new ArrayFailure("readonly binding");
1988
+ this.assertArrayWritable(state, name);
1709
1989
  if (!watch.valid())
1710
1990
  throw new ArrayFailure("stale binding");
1711
1991
  let released;
@@ -1733,18 +2013,26 @@ export class Runtime {
1733
2013
  }
1734
2014
  }
1735
2015
  async arrayJoin(owner, values, separator) {
2016
+ const raw = typeof separator !== "string" || values.some(value => typeof value !== "string");
1736
2017
  let bytes = 0;
1737
2018
  for (const value of values) {
1738
- owner.reserve({ work: value.length + 1 }).release();
1739
- bytes = exactSum(bytes, Buffer.byteLength(value));
1740
- await owner.ledger.checkpoint(this.signal, value.length + 1);
2019
+ const length = typeof value === "string" ? value.length : shellValueByteLength(value);
2020
+ owner.reserve({ work: length + 1 }).release();
2021
+ bytes = exactSum(bytes, shellValueByteLength(value));
2022
+ await owner.ledger.checkpoint(this.signal, length + 1);
1741
2023
  }
1742
- bytes = exactSum(bytes, Math.max(0, values.length - 1) * Buffer.byteLength(separator));
1743
- owner.reserve({ metadata: exactSum(96, values.length * 32), payload: bytes, allocatedSlots: values.length, work: values.length * 3 + 7 });
2024
+ bytes = exactSum(bytes, Math.max(0, values.length - 1) * shellValueByteLength(separator));
2025
+ owner.reserve({ metadata: exactSum(exactSum(96, values.length * 32), raw ? exactSum(bytes * 2, 64) : 0), payload: bytes, allocatedSlots: values.length * (raw ? 2 : 1), work: values.length * 3 + 7 });
1744
2026
  this.signal.throwIfAborted();
1745
- const result = values.join(separator);
1746
- await owner.ledger.checkpoint(this.signal);
1747
- return result;
2027
+ const allocation = raw ? this.budget.values.scope() : undefined;
2028
+ try {
2029
+ const result = raw ? concatShellValues(values.flatMap((value, index) => index ? [separator, value] : [value]), allocation) : values.join(separator);
2030
+ await owner.ledger.checkpoint(this.signal);
2031
+ return result;
2032
+ }
2033
+ finally {
2034
+ allocation?.close();
2035
+ }
1748
2036
  }
1749
2037
  async arrayIndex(binding, index, state, io, owner, create = false) {
1750
2038
  if (!binding?.associative) {
@@ -1765,16 +2053,14 @@ export class Runtime {
1765
2053
  }
1766
2054
  return binding.keyIndex(value, owner, this.signal, create);
1767
2055
  }
1768
- async arrayAssignment(assignment, state, io) {
2056
+ async arrayAssignment(assignment, state, io, declaration, origin = "assignment") {
1769
2057
  const name = assignment.name;
1770
2058
  this.signal.throwIfAborted();
1771
- if (state.readonlyVariables?.has(name)) {
1772
- if (arrayStore(state)?.get(name)?.associative) {
1773
- await this.diagnostic(io, `${name}: readonly variable`);
1774
- throw completedExit(1);
1775
- }
1776
- throw new ArrayFailure("readonly binding");
2059
+ if (state.readonlyVariables?.has(name) && arrayStore(state)?.get(name)?.associative) {
2060
+ await this.diagnostic(io, `${name}: readonly variable`);
2061
+ throw completedExit(1);
1777
2062
  }
2063
+ this.assertArrayWritable(state, name, origin);
1778
2064
  if (controlNames.has(name))
1779
2065
  throw new ArrayFailure("control binding cannot be indexed");
1780
2066
  if (state.exported.has(name))
@@ -1807,7 +2093,7 @@ export class Runtime {
1807
2093
  else {
1808
2094
  const quotedScalar = (part) => {
1809
2095
  const selector = getArraySelector(part);
1810
- return part.quoted && !(part.kind === "variable" && (part.name === "@" || selector?.kind === "members" && selector.separator === "@"));
2096
+ return part.quoted && !(part.kind === "variable" && (part.name === "@" || selector && selector.kind !== "element" && selector.separator === "@"));
1811
2097
  };
1812
2098
  const certain = entry.value.parts.every(part => part.kind === "text" ? part.quoted || !/[*?[]/u.test(part.value) : quotedScalar(part));
1813
2099
  const demanded = entry.value.parts.some(part => part.kind === "text" ? part.quoted || part.value.length > 0 : quotedScalar(part));
@@ -1824,11 +2110,17 @@ export class Runtime {
1824
2110
  const supersede = await stateMonitor(state).prepareTypedPublication(name, operation, this.signal);
1825
2111
  await this.prepareArrayObservers(state, operation);
1826
2112
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
2113
+ let frozenAttributes;
2114
+ if (declaration && !state.readonlyVariables?.has(name)) {
2115
+ store.owner.reserve({ metadata: state.readonlyVariables ? 32 : 96, slots: 1, work: 5 });
2116
+ if (!state.readonlyVariables)
2117
+ frozenAttributes = stateMonitor(state).prepareCollection(new Set(), "readonlyVariables");
2118
+ }
1827
2119
  const prepared = await store.prepareName(name, operation, this.signal);
1828
2120
  const preserve = assignment.kind === "element" || assignment.append;
1829
2121
  staged = preserve && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner, current?.associative);
1830
2122
  if (preserve && !current && state.variables[name] !== undefined) {
1831
- const token = await valueToken(staged.owner, stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], this.signal);
2123
+ const token = await textToken(staged.owner, stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], this.signal);
1832
2124
  try {
1833
2125
  staged.insert(0, token);
1834
2126
  }
@@ -1859,7 +2151,7 @@ export class Runtime {
1859
2151
  const fields = await this.valueWord(assignment.value, state, io, false);
1860
2152
  let value = await join(fields);
1861
2153
  if (assignment.append)
1862
- value = await join([staged.values.get(index)?.text.rawValue ?? staged.get(index) ?? "", value]);
2154
+ value = await join([staged.getValue(index) ?? "", value]);
1863
2155
  await insert(index, value);
1864
2156
  }
1865
2157
  else
@@ -1871,7 +2163,7 @@ export class Runtime {
1871
2163
  if (expanded === original)
1872
2164
  break;
1873
2165
  expandedEntry = true;
1874
- const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
2166
+ const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false, false);
1875
2167
  for (const value of values) {
1876
2168
  await insert(cursor, value);
1877
2169
  cursor++;
@@ -1893,17 +2185,20 @@ export class Runtime {
1893
2185
  }
1894
2186
  }
1895
2187
  this.signal.throwIfAborted();
1896
- if (state.readonlyVariables?.has(name))
1897
- throw new ArrayFailure("readonly binding");
2188
+ this.assertArrayWritable(state, name, origin);
1898
2189
  if (!watch.valid())
1899
2190
  throw new ArrayFailure("stale binding");
1900
2191
  staged.assigned = true;
1901
- if (!(assignment.kind === "compound" && assignment.append && writes === 0 && current?.assigned)) {
2192
+ if (declaration || !(assignment.kind === "compound" && assignment.append && writes === 0 && current?.assigned)) {
1902
2193
  let released;
1903
2194
  stateMonitor(state).publish(tickets, name, () => {
1904
2195
  supersede();
1905
2196
  delete state.variables[name];
1906
2197
  released = store.publish(name, staged, tickets, prepared);
2198
+ if (declaration) {
2199
+ state.readonlyVariables ??= frozenAttributes;
2200
+ state.readonlyVariables.add(name);
2201
+ }
1907
2202
  });
1908
2203
  staged = undefined;
1909
2204
  await released;
@@ -1921,87 +2216,861 @@ export class Runtime {
1921
2216
  }
1922
2217
  }
1923
2218
  async run(script, state, io) {
1924
- return (await this.runUnit(script, state, io)).exitCode;
2219
+ return this.finishShell(state, io, (await this.runUnit(script, state, io)).exitCode);
1925
2220
  }
1926
- async runUnit(script, state, io) {
1927
- state = trackState(state, this.budget, io[invocationScope]);
1928
- try {
1929
- return { exitCode: await this.script(script, state, io), terminated: false };
1930
- }
1931
- catch (error) {
1932
- if (error instanceof NounsetDiagnosticFailure) {
1933
- if (state.isolated)
2221
+ extensionContext(state, io, command = "", args = [], argumentValues = args) {
2222
+ const frame = state.extensions;
2223
+ let bindings;
2224
+ let input;
2225
+ const createBindings = this.extensionBindings.bind(this, state, io);
2226
+ const createInput = this.extensionInput.bind(this, state, io);
2227
+ const signal = AbortSignal.any([this.signal, this.budget.executionCleanup.controller.signal]);
2228
+ return {
2229
+ command, args, argumentValues, status: state.status, functionDepth: state.functionDepth, sourceDepth: state.sourceDepth ?? 0,
2230
+ stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, signal, scope: frame,
2231
+ get bindings() { return bindings ??= createBindings(); },
2232
+ get input() { return input ??= createInput(); },
2233
+ variable: name => state.variables[name],
2234
+ accountSource: source => this.budget.source(shellValueByteLength(source)),
2235
+ diagnostic: message => this.diagnostic(io, message),
2236
+ evaluate: async (source, options) => {
2237
+ this.signal.throwIfAborted();
2238
+ if (state.depth >= this.budget.limits.maxSubstitutionDepth)
2239
+ this.budget.fail("maxSubstitutionDepth");
2240
+ this.budget.source(shellValueByteLength(source));
2241
+ const byteSource = typeof source !== "string";
2242
+ const text = byteSource ? Buffer.from(shellValueBytes(source)).toString("latin1") : source;
2243
+ const restoration = stateMonitor(state)?.restoration(true);
2244
+ try {
2245
+ state.depth++;
2246
+ return await this.runCurrentText(text, state, { ...io, diagnosticOffset: (io.diagnosticLine ?? 1) - 1 }, false, options?.name === undefined ? undefined : `${io.scriptName ?? "shell"}: ${options.name}`, byteSource, false);
2247
+ }
2248
+ finally {
2249
+ if (restoration)
2250
+ restoration.apply(() => { state.depth--; });
2251
+ else
2252
+ state.depth--;
2253
+ }
2254
+ },
2255
+ registerCleanup: cleanup => {
2256
+ let completion;
2257
+ const close = () => completion ??= Promise.resolve().then(cleanup);
2258
+ frame.cleanup.push(close);
2259
+ io[invocationScope].register(close);
2260
+ },
2261
+ registerExecutionCleanup: cleanup => {
2262
+ this.budget.executionCleanup.register(cleanup);
2263
+ return AbortSignal.any([this.commandSignal, this.budget.signal, this.budget.executionCleanup.controller.signal]);
2264
+ },
2265
+ interruptWait: status => {
2266
+ validateExitCode(status);
2267
+ return !signal.aborted && (frame.waiting?.(status) ?? false);
2268
+ },
2269
+ waitInterruptibly: async (operation) => {
2270
+ signal.throwIfAborted();
2271
+ const scope = io[invocationScope];
2272
+ scope.assertOpen();
2273
+ if (typeof operation !== "function")
2274
+ throw new TypeError("Cooperative wait requires a callable operation");
2275
+ if (frame.waiting)
2276
+ throw new Error("A cooperative wait is already active in this shell scope");
2277
+ const controller = new AbortController();
2278
+ const reason = Object.freeze({});
2279
+ let interruption;
2280
+ const interrupt = (status) => {
2281
+ if (interruption || signal.aborted || scope.signal.aborted)
2282
+ return false;
2283
+ interruption = { status };
2284
+ controller.abort(reason);
2285
+ return true;
2286
+ };
2287
+ const close = () => { if (frame.waiting === interrupt)
2288
+ delete frame.waiting; };
2289
+ scope.register(close);
2290
+ frame.waiting = interrupt;
2291
+ try {
2292
+ const value = await scope.run(() => operation(AbortSignal.any([signal, scope.signal, controller.signal])));
2293
+ signal.throwIfAborted();
2294
+ return { kind: "completed", value };
2295
+ }
2296
+ catch (error) {
2297
+ signal.throwIfAborted();
2298
+ if (interruption && error === reason)
2299
+ return { kind: "interrupted", status: interruption.status };
1934
2300
  throw error;
1935
- throw error.reason;
1936
- }
1937
- if (error instanceof Flow && error.kind === "exit")
1938
- return { exitCode: error.status, terminated: true };
1939
- throw error;
1940
- }
2301
+ }
2302
+ finally {
2303
+ close();
2304
+ }
2305
+ },
2306
+ };
1941
2307
  }
1942
- async script(script, state, io) {
1943
- for (const list of script.lists) {
1944
- if (list.background) {
1945
- const jobs = state.backgroundJobs;
1946
- const id = jobs.execution.identifier();
1947
- const scope = jobs.execution.scope.child();
1948
- const inheritedDestinations = new Set([io.stdout, io.stderr, ...[...io.descriptors?.values() ?? []].flatMap(descriptor => descriptor.output && !descriptor.closed ? [descriptor.output] : [])].map(backgroundDestination));
1949
- const leases = (io[backgroundResources] ?? []).filter(resource => !resource.destination || inheritedDestinations.has(resource.destination)).map(resource => ({ resource, release: resource.retain() }));
1950
- const releases = leases.map(lease => lease.release);
1951
- let child;
2308
+ extensionBindings(state, io) {
2309
+ const scope = io[invocationScope];
2310
+ scope.assertOpen();
2311
+ const allocation = this.budget.values.scope();
2312
+ const references = new Set();
2313
+ scope.register(async () => {
2314
+ try {
2315
+ await Promise.all([...references].map(close => close()));
2316
+ }
2317
+ finally {
2318
+ allocation.close();
2319
+ }
2320
+ });
2321
+ const assertOpen = () => { this.signal.throwIfAborted(); scope.assertOpen(); allocation.assertOpen(); };
2322
+ const checkName = (name) => {
2323
+ assertOpen();
2324
+ if (typeof name !== "string" || !/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(name))
2325
+ throw new TypeError("Invalid binding name");
2326
+ };
2327
+ const checkIndex = (index, maximum = 2147483647) => {
2328
+ if (!Number.isSafeInteger(index) || index < 0 || index > maximum)
2329
+ throw new RangeError(`Binding index outside 0..${maximum}`);
2330
+ };
2331
+ const retain = (value) => {
2332
+ if (value !== undefined && typeof value !== "string")
2333
+ allocation.hold(value);
2334
+ return value;
2335
+ };
2336
+ return Object.freeze({
2337
+ describe: (name) => {
2338
+ checkName(name);
2339
+ const kind = arrayStore(state)?.get(name) ? "indexed" : state.variables[name] === undefined ? "unset" : "scalar";
2340
+ return Object.freeze({ kind, readonly: state.readonlyVariables?.has(name) ?? false, exported: state.exported.has(name) });
2341
+ },
2342
+ get: (name, index = 0) => {
2343
+ checkName(name);
2344
+ checkIndex(index, 4294967295);
2345
+ const binding = arrayStore(state)?.get(name);
2346
+ const scalar = state.variables[name];
2347
+ return retain(binding ? binding.getValue(index) : index === 0 && scalar !== undefined ? stateMonitor(state)?.values.get(name, scalar) ?? scalar : undefined);
2348
+ },
2349
+ assign: async (name, value) => {
2350
+ checkName(name);
2351
+ await scope.run(() => this.assignVariable(state, name, value));
2352
+ assertOpen();
2353
+ },
2354
+ prepareReference: async (reference) => {
2355
+ assertOpen();
2356
+ let owned;
2357
+ let closed = false;
2358
+ let completion;
2359
+ const work = new Set();
2360
+ const close = () => {
2361
+ closed = true;
2362
+ return completion ??= Promise.resolve().then(async () => {
2363
+ await Promise.allSettled([...work]);
2364
+ try {
2365
+ owned?.close();
2366
+ }
2367
+ finally {
2368
+ references.delete(close);
2369
+ }
2370
+ });
2371
+ };
2372
+ references.add(close);
2373
+ const failure = (value, suffix, prefix = "") => {
2374
+ const diagnostic = concatShellValues([prefix, value, suffix], allocation);
2375
+ allocation.hold(diagnostic);
2376
+ return Object.freeze({ ok: false, diagnostic });
2377
+ };
1952
2378
  try {
1953
- child = await cloneState(state, this.signal, scope);
2379
+ owned = this.budget.values.scope();
2380
+ owned.hold(reference);
2381
+ const text = shellValueText(reference);
2382
+ const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[([^\[\]]+)\])?$/u.exec(text);
2383
+ if (!match) {
2384
+ const result = failure(reference, "': not a valid identifier", "`");
2385
+ await close();
2386
+ return result;
2387
+ }
2388
+ const name = match[1];
2389
+ const subscript = match[2];
2390
+ const reservation = owned.reserve(128 + text.length * 2, 1);
2391
+ const run = (action) => {
2392
+ try {
2393
+ assertOpen();
2394
+ if (closed)
2395
+ throw new Error("Binding reference is closed");
2396
+ if (work.size)
2397
+ throw new Error("Binding reference is busy");
2398
+ const pending = scope.run(() => Promise.resolve().then(() => {
2399
+ this.signal.throwIfAborted();
2400
+ return action();
2401
+ }));
2402
+ work.add(pending);
2403
+ void pending.then(() => { work.delete(pending); }, () => { work.delete(pending); });
2404
+ return pending;
2405
+ }
2406
+ catch (error) {
2407
+ return Promise.reject(error);
2408
+ }
2409
+ };
2410
+ const handle = Object.freeze({
2411
+ unbindName: () => run(async () => {
2412
+ if (state.readonlyVariables?.has(text))
2413
+ return failure(reference, ": cannot unset: readonly variable");
2414
+ if (arrayStore(state)?.get(text))
2415
+ await this.unsetIndexed(state, text);
2416
+ else
2417
+ this.unsetVariable(state, text);
2418
+ return Object.freeze({ ok: true, value: undefined });
2419
+ }),
2420
+ assignInteger: (value) => run(async () => {
2421
+ if (!Number.isSafeInteger(value))
2422
+ throw new TypeError("Binding integer must be a safe integer");
2423
+ if (state.readonlyVariables?.has(name))
2424
+ return failure(name, ": readonly variable");
2425
+ if (subscript === undefined)
2426
+ await this.assignVariable(state, name, String(value));
2427
+ else {
2428
+ const index = literalIndex(subscript, name.length + 1);
2429
+ await this.arrayAssignment({
2430
+ kind: "element", name, index, append: false,
2431
+ value: { offset: 0, parts: [{ kind: "text", value: String(value), quoted: true }] },
2432
+ }, state, io);
2433
+ }
2434
+ return Object.freeze({ ok: true, value: undefined });
2435
+ }),
2436
+ close,
2437
+ });
2438
+ reservation.commit(handle);
2439
+ return Object.freeze({ ok: true, value: handle });
1954
2440
  }
1955
2441
  catch (error) {
1956
- releases.forEach(release => release());
1957
- await scope.close();
2442
+ try {
2443
+ await close();
2444
+ }
2445
+ catch (cleanup) {
2446
+ scope.failures.push(cleanup);
2447
+ }
1958
2448
  throw error;
1959
2449
  }
1960
- child.isolated = true;
1961
- child.loopDepth = 0;
1962
- const inherited = isolateIO(activeIO(io));
1963
- const redirectsInput = list.pipelines[0].commands[0].redirects.some(redirect => redirect.descriptor === 0);
1964
- const stdin = redirectsInput ? inherited.stdin : toByteSource("");
1965
- const descriptors = new Map(inherited.descriptors);
1966
- descriptors.set(0, { input: stdin, stdinIsDefault: false });
1967
- let initialRedirection = true;
1968
- const backgroundIO = { ...inherited, [invocationScope]: scope, stdin, stdinIsDefault: false, descriptors,
1969
- [backgroundPrune]: list.pipelines.length === 1 ? descriptors => {
1970
- if (!initialRedirection)
1971
- return;
1972
- initialRedirection = false;
1973
- const destinations = new Set([...descriptors.values()].flatMap(descriptor => descriptor.output && !descriptor.closed ? [backgroundDestination(descriptor.output)] : []));
1974
- for (const lease of leases)
1975
- if (lease.resource.destination && !destinations.has(lease.resource.destination))
1976
- lease.release();
1977
- } : undefined,
2450
+ },
2451
+ openIndexed: async (name, options = {}) => {
2452
+ checkName(name);
2453
+ const clear = options.clear;
2454
+ if (clear !== undefined && typeof clear !== "boolean")
2455
+ throw new TypeError("Invalid indexed writer options");
2456
+ return this.incrementalIndexed(state, io, name, clear === true);
2457
+ },
2458
+ prepare: async (name, options) => {
2459
+ checkName(name);
2460
+ if (options.kind !== "indexed" || options.clear !== undefined && typeof options.clear !== "boolean")
2461
+ throw new TypeError("Unsupported binding preparation");
2462
+ if (state.readonlyVariables?.has(name))
2463
+ throw new ArrayFailure("readonly binding");
2464
+ if (controlNames.has(name))
2465
+ throw new ArrayFailure("control binding cannot be indexed");
2466
+ if (state.exported.has(name))
2467
+ throw new ArrayFailure("exported binding cannot be indexed");
2468
+ const clear = options.clear === true;
2469
+ const store = requireArrays(state);
2470
+ let prepared;
2471
+ let preparing;
2472
+ let active;
2473
+ let closed = false;
2474
+ let completion;
2475
+ const close = () => {
2476
+ closed = true;
2477
+ return completion ??= (async () => {
2478
+ await Promise.allSettled([preparing, active]);
2479
+ await prepared?.close();
2480
+ })();
2481
+ };
2482
+ scope.register(close);
2483
+ const validate = () => {
2484
+ assertOpen();
2485
+ if (closed)
2486
+ throw new ArrayFailure("binding transaction is closed");
2487
+ if (state.readonlyVariables?.has(name))
2488
+ throw new ArrayFailure("readonly binding");
2489
+ if (state.exported.has(name))
2490
+ throw new ArrayFailure("exported binding cannot be indexed");
2491
+ prepared.validate();
2492
+ };
2493
+ const run = (action) => {
2494
+ try {
2495
+ validate();
2496
+ if (active)
2497
+ throw new ArrayFailure("binding transaction is busy");
2498
+ const work = scope.run(action);
2499
+ active = work;
2500
+ void work.then(() => { if (active === work)
2501
+ active = undefined; }, () => { if (active === work)
2502
+ active = undefined; });
2503
+ return work;
2504
+ }
2505
+ catch (error) {
2506
+ return Promise.reject(error);
2507
+ }
1978
2508
  };
1979
- const runtime = new Runtime(runtimeFileSystems.get(this.fs) ?? this.fs, this.commands, this.middleware, this.budget, AbortSignal.any([this.commandSignal, scope.signal]), this.fileWrites, this.outputFiles, this.commandSignal, this.cancellation, this.cancellationState, this.cancellationOwner, this.cancellationDepth, this.cancellationMaxDepth, undefined, this.inputProfile);
1980
2509
  try {
1981
- state.lastBackgroundJob = jobs.launch(id, async () => {
1982
- try {
2510
+ preparing = scope.run(async () => {
2511
+ prepared = await store.prepare(name, this.signal, !clear);
2512
+ validate();
2513
+ if (!clear && !store.get(name) && state.variables[name] !== undefined) {
2514
+ const value = stateMonitor(state).values.get(name, state.variables[name]);
2515
+ const token = await textToken(prepared.binding.owner, value, this.signal);
1983
2516
  try {
1984
- return (await runtime.runUnit({ lists: [{ ...list, background: false }] }, child, backgroundIO)).exitCode;
2517
+ validate();
2518
+ prepared.binding.insert(0, token);
1985
2519
  }
1986
2520
  catch (error) {
1987
- if (error instanceof Flow && error.kind === "return")
1988
- return error.status;
2521
+ token.release();
1989
2522
  throw error;
1990
2523
  }
1991
2524
  }
1992
- finally {
1993
- releases.forEach(release => release());
1994
- await scope.close();
1995
- }
2525
+ validate();
2526
+ });
2527
+ await preparing;
2528
+ return Object.freeze({
2529
+ get: (index) => { validate(); checkIndex(index); return retain(prepared.binding.getValue(index)); },
2530
+ set: (index, value) => run(async () => {
2531
+ checkIndex(index);
2532
+ const token = await textToken(prepared.binding.owner, value, this.signal);
2533
+ try {
2534
+ validate();
2535
+ prepared.binding.insert(index, token);
2536
+ }
2537
+ catch (error) {
2538
+ token.release();
2539
+ throw error;
2540
+ }
2541
+ }),
2542
+ unset: (index) => run(async () => {
2543
+ checkIndex(index);
2544
+ const binding = prepared.binding;
2545
+ binding.values.get(index)?.slot.release();
2546
+ let maximum = -1;
2547
+ for (const key of binding.values.keys()) {
2548
+ maximum = Math.max(maximum, key);
2549
+ await binding.owner.ledger.checkpoint(this.signal, 2);
2550
+ validate();
2551
+ }
2552
+ binding.maximum = maximum;
2553
+ }),
2554
+ commit: () => run(async () => {
2555
+ await this.prepareArrayObservers(state, prepared.binding.owner);
2556
+ const supersede = await stateMonitor(state).prepareTypedPublication(name, prepared.binding.owner, this.signal);
2557
+ validate();
2558
+ let retirement;
2559
+ stateMonitor(state).publish(prepared.tickets, name, () => {
2560
+ supersede();
2561
+ delete state.variables[name];
2562
+ retirement = prepared.publish();
2563
+ });
2564
+ await retirement;
2565
+ this.signal.throwIfAborted();
2566
+ }),
2567
+ close,
1996
2568
  });
1997
2569
  }
1998
2570
  catch (error) {
1999
- releases.forEach(release => release());
2000
- await scope.close();
2571
+ await close();
2001
2572
  throw error;
2002
2573
  }
2003
- state.status = 0;
2004
- continue;
2574
+ },
2575
+ });
2576
+ }
2577
+ async incrementalIndexed(state, io, name, clear) {
2578
+ const scope = io[invocationScope];
2579
+ scope.assertOpen();
2580
+ this.signal.throwIfAborted();
2581
+ if (state.readonlyVariables?.has(name))
2582
+ throw new ArrayFailure("readonly binding");
2583
+ if (controlNames.has(name))
2584
+ throw new ArrayFailure("control binding cannot be indexed");
2585
+ const store = requireArrays(state);
2586
+ const owner = store.owner;
2587
+ const monitor = stateMonitor(state);
2588
+ let lifetime;
2589
+ let holding;
2590
+ let identity;
2591
+ let admittedLocal;
2592
+ let preparing;
2593
+ let active;
2594
+ let closed = false;
2595
+ let completion;
2596
+ const close = () => {
2597
+ closed = true;
2598
+ return completion ??= (async () => {
2599
+ await Promise.allSettled([preparing, active]);
2600
+ try {
2601
+ await lifetime?.close();
2602
+ }
2603
+ finally {
2604
+ holding?.release();
2605
+ }
2606
+ })();
2607
+ };
2608
+ scope.register(close);
2609
+ const localIdentity = () => {
2610
+ for (let index = state.locals.length - 1; index >= 0; index--) {
2611
+ owner.reserve({ work: 2 }).release();
2612
+ const saved = state.locals[index].get(name);
2613
+ if (saved)
2614
+ return saved;
2615
+ }
2616
+ return undefined;
2617
+ };
2618
+ const validate = () => {
2619
+ this.signal.throwIfAborted();
2620
+ scope.assertOpen();
2621
+ if (closed)
2622
+ throw new ArrayFailure("indexed writer is closed");
2623
+ owner.assertOpen();
2624
+ if (arrayStore(state) !== store || store.owner !== owner || identity !== undefined && store.bindings.get(name) !== identity)
2625
+ throw new ArrayFailure("incremental target identity changed");
2626
+ if (localIdentity() !== admittedLocal)
2627
+ throw new ArrayFailure("incremental target local identity changed");
2628
+ };
2629
+ try {
2630
+ admittedLocal = localIdentity();
2631
+ preparing = scope.run(async () => {
2632
+ holding = owner.hold();
2633
+ lifetime = ArrayOwner.create(owner.ledger, owner);
2634
+ lifetime.reserve({ metadata: 192, work: 12 });
2635
+ await textToken(lifetime, name, this.signal);
2636
+ const watch = await store.watch(name, lifetime, this.signal);
2637
+ try {
2638
+ await this.prepareArrayObservers(state, lifetime);
2639
+ validate();
2640
+ if (state.readonlyVariables?.has(name))
2641
+ throw new ArrayFailure("readonly binding");
2642
+ if (!watch.valid())
2643
+ throw new ArrayFailure("binding changed during writer admission");
2644
+ if (clear || !store.get(name)) {
2645
+ const prepared = await store.prepare(name, this.signal, false);
2646
+ try {
2647
+ if (!clear && state.variables[name] !== undefined) {
2648
+ const value = monitor.values.get(name, state.variables[name]);
2649
+ const token = await textToken(prepared.binding.owner, value, this.signal);
2650
+ try {
2651
+ prepared.validate();
2652
+ prepared.binding.insert(0, token);
2653
+ }
2654
+ catch (error) {
2655
+ token.release();
2656
+ throw error;
2657
+ }
2658
+ }
2659
+ validate();
2660
+ if (state.readonlyVariables?.has(name))
2661
+ throw new ArrayFailure("readonly binding");
2662
+ if (!watch.valid())
2663
+ throw new ArrayFailure("binding changed during writer admission");
2664
+ prepared.validate();
2665
+ let retirement;
2666
+ monitor.publish(prepared.tickets, name, () => {
2667
+ delete state.variables[name];
2668
+ retirement = prepared.publish();
2669
+ });
2670
+ await retirement;
2671
+ }
2672
+ finally {
2673
+ await prepared.close();
2674
+ }
2675
+ }
2676
+ identity = store.bindings.get(name);
2677
+ if (!identity)
2678
+ throw new ArrayFailure("incremental target is not indexed");
2679
+ validate();
2680
+ }
2681
+ finally {
2682
+ watch.close();
2683
+ }
2684
+ });
2685
+ await preparing;
2686
+ }
2687
+ catch (error) {
2688
+ try {
2689
+ await close();
2690
+ }
2691
+ catch (cleanup) {
2692
+ scope.failures.push(cleanup);
2693
+ }
2694
+ throw error;
2695
+ }
2696
+ return Object.freeze({
2697
+ set: (index, value) => {
2698
+ try {
2699
+ validate();
2700
+ if (!Number.isSafeInteger(index) || index < 0 || index > 4294967295)
2701
+ throw new ArrayFailure("index outside 0..4294967295");
2702
+ if (active)
2703
+ throw new ArrayFailure("indexed writer is busy");
2704
+ const work = scope.run(async () => {
2705
+ const current = store.get(name);
2706
+ const operation = ArrayOwner.create(owner.ledger, lifetime);
2707
+ let retained = false;
2708
+ let staged;
2709
+ let token;
2710
+ try {
2711
+ current.retain();
2712
+ retained = true;
2713
+ const watch = await store.watch(name, operation, this.signal);
2714
+ const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
2715
+ await this.prepareArrayObservers(state, operation);
2716
+ token = await textToken(operation, value, this.signal);
2717
+ validate();
2718
+ if (!watch.valid() || store.get(name) !== current)
2719
+ throw new ArrayFailure("incremental target changed during write");
2720
+ if (current.references > 2)
2721
+ staged = await current.copy(this.signal);
2722
+ validate();
2723
+ if (!watch.valid() || store.get(name) !== current)
2724
+ throw new ArrayFailure("incremental target changed during write");
2725
+ const target = staged ?? current;
2726
+ let retirement;
2727
+ monitor.publish(tickets, name, () => {
2728
+ target.insert(index, token);
2729
+ token = undefined;
2730
+ if (staged)
2731
+ retirement = store.publish(name, staged, tickets);
2732
+ else
2733
+ store.revise(name, current, tickets);
2734
+ });
2735
+ staged = undefined;
2736
+ watch.close();
2737
+ await retirement;
2738
+ this.signal.throwIfAborted();
2739
+ }
2740
+ finally {
2741
+ token?.release();
2742
+ try {
2743
+ await staged?.release();
2744
+ }
2745
+ finally {
2746
+ try {
2747
+ if (retained)
2748
+ await current.release();
2749
+ }
2750
+ finally {
2751
+ await operation.close();
2752
+ }
2753
+ }
2754
+ }
2755
+ });
2756
+ active = work;
2757
+ void work.then(() => { if (active === work)
2758
+ active = undefined; }, () => { if (active === work)
2759
+ active = undefined; });
2760
+ return work;
2761
+ }
2762
+ catch (error) {
2763
+ return Promise.reject(error);
2764
+ }
2765
+ },
2766
+ close,
2767
+ });
2768
+ }
2769
+ extensionInput(state, io) {
2770
+ const scope = io[invocationScope];
2771
+ const assertOpen = () => { this.signal.throwIfAborted(); scope.assertOpen(); };
2772
+ assertOpen();
2773
+ const checkDescriptor = (descriptor) => {
2774
+ assertOpen();
2775
+ if (!Number.isSafeInteger(descriptor) || descriptor < 0)
2776
+ throw new RangeError("Invalid input descriptor");
2777
+ };
2778
+ return Object.freeze({ validateOpen: (descriptor) => {
2779
+ checkDescriptor(descriptor);
2780
+ const entry = io.descriptors?.get(descriptor);
2781
+ const input = entry?.input ?? (!io.descriptors && descriptor === 0 ? io.stdin : undefined);
2782
+ const output = entry?.output ?? (!io.descriptors ? descriptor === 1 ? io.stdout : descriptor === 2 ? io.stderr : undefined : undefined);
2783
+ if (entry?.closed || (!input || input === closedSource) && (!output || output === closedSink))
2784
+ throw new FsError("EBADF", { message: "Closed input descriptor" });
2785
+ }, observe: (descriptor) => {
2786
+ checkDescriptor(descriptor);
2787
+ const entry = io.descriptors?.get(descriptor);
2788
+ const input = entry?.input ?? (!io.descriptors && descriptor === 0 ? io.stdin : undefined);
2789
+ const output = entry?.output ?? (!io.descriptors ? descriptor === 1 ? io.stdout : descriptor === 2 ? io.stderr : undefined : undefined);
2790
+ if (entry?.closed || (!input || input === closedSource) && (!output || output === closedSink))
2791
+ throw new FsError("EBADF", { syscall: "observe" });
2792
+ const readable = input !== undefined && input !== closedSource;
2793
+ if (entry?.pipe)
2794
+ return observeDescriptor(pipeObservation(entry.pipe.endpoint, input instanceof ShellInput ? () => input.probeRead() : undefined), scope, this.budget, this.signal);
2795
+ const file = (input instanceof ShellInput ? input.descriptor : undefined) ?? entry?.file;
2796
+ const stat = file?.stat.bind(file);
2797
+ const method = file?.capabilities.readObservation === true ? file.probeRead : undefined;
2798
+ if (file?.capabilities.readObservation === true && typeof method !== "function")
2799
+ throw new FsError("ENOTSUP", { syscall: "probeRead" });
2800
+ const probe = method?.bind(file);
2801
+ return observeDescriptor({ readable, async probeRead(signal) {
2802
+ signal.throwIfAborted();
2803
+ const local = input instanceof ShellInput ? input.probeRead() : undefined;
2804
+ if (!file && local)
2805
+ return local;
2806
+ const timeout = local?.timeout ?? (stat && (await stat({ signal })).type === "file" ? "ignore" : "unknown");
2807
+ if (input instanceof ShellInput && input.bufferedBytes > 0)
2808
+ return { readiness: "ready", timeout };
2809
+ if (probe)
2810
+ return { readiness: await probe({ signal }), timeout };
2811
+ if (local)
2812
+ return local;
2813
+ return { readiness: timeout === "ignore" ? "ready" : "unknown", timeout };
2814
+ } }, scope, this.budget, this.signal);
2815
+ }, borrow: (descriptor) => {
2816
+ checkDescriptor(descriptor);
2817
+ const entry = io.descriptors?.get(descriptor);
2818
+ const source = entry?.closed ? undefined : entry?.input ?? (descriptor === 0 && !io.descriptors ? io.stdin : undefined);
2819
+ if (!source)
2820
+ throw new FsError("EBADF", { message: "Unreadable input descriptor" });
2821
+ if (!(source instanceof ShellInput))
2822
+ throw new FsError("ENOTSUP", { message: "Input descriptor has no enrolled shared cursor" });
2823
+ const resource = {};
2824
+ let closed = false;
2825
+ let completion;
2826
+ const release = () => {
2827
+ closed = true;
2828
+ return completion ??= Promise.resolve().then(() => resource.input?.close());
2829
+ };
2830
+ scope.register(release);
2831
+ const input = new ShellInput(source, this.budget, this.signal);
2832
+ resource.input = input;
2833
+ const stdinIsDefault = entry?.stdinIsDefault ?? (descriptor === 0 ? io.stdinIsDefault : undefined);
2834
+ return Object.freeze({
2835
+ ...(stdinIsDefault === undefined ? {} : { stdinIsDefault }),
2836
+ read: async (raw, options = {}) => {
2837
+ assertOpen();
2838
+ if (closed)
2839
+ throw new Error("Input borrow is closed");
2840
+ const { count, delimiter, exact, timeoutMs } = options;
2841
+ if (typeof raw !== "boolean" || count !== undefined && (!Number.isSafeInteger(count) || count < 0) || delimiter !== undefined && (!Number.isInteger(delimiter) || delimiter < 0 || delimiter > 255) || exact !== undefined && typeof exact !== "boolean" || timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0))
2842
+ throw new TypeError("Invalid input read options");
2843
+ return scope.run(() => input.line(raw, { ...(count === undefined ? {} : { count }), ...(delimiter === undefined ? {} : { delimiter }), ...(exact === undefined ? {} : { exact }), ...(timeoutMs === undefined ? {} : { timeoutMs }), byteCount: byteLocale(state.variables) }));
2844
+ },
2845
+ record: async (options = {}) => {
2846
+ assertOpen();
2847
+ if (closed)
2848
+ throw new Error("Input borrow is closed");
2849
+ const { delimiter } = options;
2850
+ if (delimiter !== undefined && (!Number.isInteger(delimiter) || delimiter < 0 || delimiter > 255))
2851
+ throw new TypeError("Invalid input record options");
2852
+ return scope.run(() => input.record(delimiter === undefined ? {} : { delimiter }));
2853
+ },
2854
+ readiness: () => {
2855
+ assertOpen();
2856
+ if (closed)
2857
+ throw new Error("Input borrow is closed");
2858
+ return input.readiness();
2859
+ },
2860
+ release,
2861
+ });
2862
+ } });
2863
+ }
2864
+ async startExtensions(state, io) {
2865
+ const frame = state.extensions;
2866
+ if (!frame || frame.started)
2867
+ return;
2868
+ frame.started = true;
2869
+ for (const [name, builtin] of frame.builtins)
2870
+ if (shellBuiltinNames.has(name) && builtin.replace !== true)
2871
+ throw new TypeError(`Extension builtin conflicts with existing builtin: ${name}`);
2872
+ for (const entry of frame.entries)
2873
+ await entry.instance.start?.(this.extensionContext(state, io));
2874
+ }
2875
+ async extensionEvent(event, state, io, status, command = "") {
2876
+ if (!state.extensions)
2877
+ return false;
2878
+ this.signal.throwIfAborted();
2879
+ await this.startExtensions(state, io);
2880
+ const previous = state.status;
2881
+ state.status = status;
2882
+ state.extensions.eventDepth = (state.extensions.eventDepth ?? 0) + 1;
2883
+ try {
2884
+ for (const entry of state.extensions.entries) {
2885
+ this.signal.throwIfAborted();
2886
+ const result = await entry.instance.event?.(event, this.extensionContext(state, io, command));
2887
+ if (result !== undefined) {
2888
+ if (event !== "command" || !result || (result.action !== "skip" && result.action !== "return"))
2889
+ throw new TypeError("Invalid extension lifecycle control");
2890
+ if (result.action === "return")
2891
+ throw new Flow("return", validateExitCode(result.status), 1, status);
2892
+ return true;
2893
+ }
2894
+ }
2895
+ return false;
2896
+ }
2897
+ finally {
2898
+ state.extensions.eventDepth--;
2899
+ if (!this.signal.aborted)
2900
+ state.status = previous;
2901
+ }
2902
+ }
2903
+ async extensionCheckpoint(point, state, io) {
2904
+ try {
2905
+ for (const checkpoint of state.extensions.checkpoints) {
2906
+ await io[invocationScope].run(() => Promise.resolve().then(async () => {
2907
+ const context = this.extensionContext(state, io);
2908
+ context.signal.throwIfAborted();
2909
+ await checkpoint(point, context);
2910
+ context.signal.throwIfAborted();
2911
+ }));
2912
+ }
2913
+ }
2914
+ catch (reason) {
2915
+ this.signal.throwIfAborted();
2916
+ if (reason instanceof Flow)
2917
+ throw reason;
2918
+ throw new ExtensionCheckpointFailure(reason);
2919
+ }
2920
+ }
2921
+ async beginShellExit(state, io, status) {
2922
+ const frame = state.extensions;
2923
+ if (!frame || frame.exiting)
2924
+ return status;
2925
+ if (frame.exitStatus !== undefined)
2926
+ return frame.exitStatus;
2927
+ this.signal.throwIfAborted();
2928
+ state = trackState(state, this.budget, io[invocationScope]);
2929
+ frame.exiting = true;
2930
+ try {
2931
+ try {
2932
+ await this.extensionEvent("exit", state, io, status);
2933
+ }
2934
+ catch (error) {
2935
+ if (error instanceof Flow && error.kind === "exit" && !(error instanceof ExtensionCheckpointFailure)) {
2936
+ if (!error.expansionFailure)
2937
+ status = error.status;
2938
+ }
2939
+ else
2940
+ throw error;
2941
+ }
2942
+ this.signal.throwIfAborted();
2943
+ }
2944
+ catch (reason) {
2945
+ extensionExitFailures.set(frame, { reason });
2946
+ }
2947
+ finally {
2948
+ frame.exiting = false;
2949
+ }
2950
+ frame.exitStatus = status;
2951
+ return status;
2952
+ }
2953
+ async finishShell(state, io, status) {
2954
+ const frame = state.extensions;
2955
+ if (!frame || frame.exiting)
2956
+ return status;
2957
+ let failure;
2958
+ try {
2959
+ status = await this.beginShellExit(state, io, status);
2960
+ failure = extensionExitFailures.get(frame);
2961
+ }
2962
+ catch (reason) {
2963
+ failure = { reason };
2964
+ }
2965
+ try {
2966
+ await this.releaseExtensions(state);
2967
+ }
2968
+ catch (cleanup) {
2969
+ this.signal.throwIfAborted();
2970
+ if (failure?.reason instanceof ExtensionCheckpointFailure)
2971
+ io[invocationScope].failures.push(cleanup);
2972
+ else {
2973
+ if (failure)
2974
+ throw new AggregateError([failure.reason, cleanup], "Shell completion and extension cleanup failed");
2975
+ throw cleanup;
2976
+ }
2977
+ }
2978
+ this.signal.throwIfAborted();
2979
+ if (failure)
2980
+ throw failure.reason instanceof ExtensionCheckpointFailure && !state.isolated ? failure.reason.reason : failure.reason;
2981
+ return status;
2982
+ }
2983
+ async releaseExtensions(state) {
2984
+ const cleanup = await Promise.allSettled(state.extensions?.cleanup.map(close => close()) ?? []);
2985
+ this.signal.throwIfAborted();
2986
+ throwCleanupFailures(cleanup.filter(result => result.status === "rejected").map(result => result.reason));
2987
+ }
2988
+ async finishReturn(event, state, io, status, previous = status) {
2989
+ while (true) {
2990
+ try {
2991
+ await this.extensionEvent(event, state, io, previous);
2992
+ return status;
2993
+ }
2994
+ catch (error) {
2995
+ if (error instanceof ExtensionCheckpointFailure)
2996
+ throw error;
2997
+ if (error instanceof Flow && error.kind === "return") {
2998
+ status = error.status;
2999
+ previous = error.previousStatus ?? status;
3000
+ continue;
3001
+ }
3002
+ if (error instanceof Flow && error.kind === "exit")
3003
+ throw new Flow("exit", await this.beginShellExit(state, io, error.status), 1, error.previousStatus);
3004
+ throw error;
3005
+ }
3006
+ }
3007
+ }
3008
+ async runUnit(script, state, io) {
3009
+ state = trackState(state, this.budget, io[invocationScope]);
3010
+ try {
3011
+ await this.startExtensions(state, io);
3012
+ return { exitCode: await this.inputUnit(script, state, io), terminated: false };
3013
+ }
3014
+ catch (error) {
3015
+ if (error instanceof NounsetDiagnosticFailure) {
3016
+ if (state.isolated)
3017
+ throw error;
3018
+ throw error.reason;
3019
+ }
3020
+ if (error instanceof Flow && error.kind === "exit")
3021
+ return { exitCode: await this.finishShell(state, io, error.status), terminated: true };
3022
+ throw error;
3023
+ }
3024
+ }
3025
+ async inputUnit(script, state, io) {
3026
+ try {
3027
+ return await this.script(script, state, io);
3028
+ }
3029
+ catch (error) {
3030
+ if (!(error instanceof Flow) || error.kind !== "discard")
3031
+ throw error;
3032
+ this.signal.throwIfAborted();
3033
+ throwCleanupFailures(io[invocationScope].failures);
3034
+ state.status = error.status;
3035
+ return error.status;
3036
+ }
3037
+ }
3038
+ async script(script, state, io) {
3039
+ if (state.extensions?.syntax.indexedDeclarations?.includes("readonly"))
3040
+ io.assignmentDiagnosticContext ??= { name: undefined };
3041
+ for (const list of script.lists) {
3042
+ if (list.terminator) {
3043
+ const hook = state.extensions?.listTerminators.get(list.terminator.operator);
3044
+ if (!hook)
3045
+ throw new TypeError("Missing captured shell list terminator handler");
3046
+ let admissionOpen = true;
3047
+ let preparation;
3048
+ let failure;
3049
+ let status = 0;
3050
+ try {
3051
+ status = validateExitCode(await hook.execute({
3052
+ ...this.extensionContext(state, io),
3053
+ prepareChild: options => {
3054
+ if (!admissionOpen || preparation)
3055
+ return Promise.reject(this.commandSignal.aborted ? this.commandSignal.reason : new TypeError("Shell child preparation is single-use and dispatch-scoped"));
3056
+ preparation = this.prepareListChild(list, script, state, io, options);
3057
+ void preparation.catch(() => undefined);
3058
+ return preparation;
3059
+ },
3060
+ }));
3061
+ }
3062
+ catch (reason) {
3063
+ failure = { reason };
3064
+ }
3065
+ finally {
3066
+ admissionOpen = false;
3067
+ }
3068
+ await preparation?.catch(() => undefined);
3069
+ this.commandSignal.throwIfAborted();
3070
+ if (failure)
3071
+ throw failure.reason;
3072
+ state.status = status;
3073
+ continue;
2005
3074
  }
2006
3075
  for (let index = 0; index < list.pipelines.length; index++) {
2007
3076
  const operator = list.operators[index - 1];
@@ -2024,11 +3093,108 @@ export class Runtime {
2024
3093
  }
2025
3094
  return script.lists.length ? state.status : 0;
2026
3095
  }
3096
+ async prepareListChild(list, script, state, io, options) {
3097
+ this.signal.throwIfAborted();
3098
+ if (!options || !(options.signal instanceof AbortSignal) || typeof options.registerCleanup !== "function"
3099
+ || options.stdin !== "inherit" && options.stdin !== "async-default")
3100
+ throw new TypeError("Invalid shell child preparation");
3101
+ const signal = AbortSignal.any([this.commandSignal, options.signal, this.budget.executionCleanup.controller.signal]);
3102
+ signal.throwIfAborted();
3103
+ const scope = new InvocationScope(signal);
3104
+ const references = new PipeDescriptorFrame(scope);
3105
+ const descriptors = new PreparedDescriptorFrame(references, this.budget);
3106
+ let child;
3107
+ let runtime;
3108
+ let execution;
3109
+ let closing;
3110
+ let closed = false;
3111
+ let ready;
3112
+ const acquisition = new Promise(resolve => { ready = resolve; });
3113
+ const close = () => {
3114
+ closed = true;
3115
+ return closing ??= Promise.resolve().then(async () => {
3116
+ await acquisition;
3117
+ await execution?.catch(() => undefined);
3118
+ await scope.close();
3119
+ if (child)
3120
+ stateMonitor(child)?.closeValues();
3121
+ throwCleanupFailures(scope.failures);
3122
+ });
3123
+ };
3124
+ try {
3125
+ options.registerCleanup(close);
3126
+ if (state.depth >= this.budget.limits.maxSubstitutionDepth)
3127
+ this.budget.fail("maxSubstitutionDepth");
3128
+ child = await cloneState(state, signal);
3129
+ if (closed)
3130
+ throw new TypeError("Shell child preparation is closed");
3131
+ signal.throwIfAborted();
3132
+ child.extensions = forkExtensions(state.extensions, "subshell");
3133
+ child.isolated = true;
3134
+ child.depth++;
3135
+ const allocation = this.budget.values.scope();
3136
+ scope.register(() => allocation.close());
3137
+ const childIO = isolateIO({ ...io, [invocationScope]: scope, [valueScope]: allocation }, references);
3138
+ descriptors.acquire(childIO.descriptors);
3139
+ Object.assign(childIO, { terminal: list.pipelines.length === 1 ? { target: list.pipelines[0], frame: descriptors } : undefined });
3140
+ if (options.stdin === "async-default") {
3141
+ const input = new ShellInput((async function* () { })(), this.budget, signal);
3142
+ scope.register(async () => {
3143
+ try {
3144
+ await input.close();
3145
+ }
3146
+ catch (reason) {
3147
+ if (!signal.aborted || !Object.is(reason, signal.reason))
3148
+ throw reason;
3149
+ }
3150
+ });
3151
+ Object.assign(childIO, { asyncDefaultInput: input });
3152
+ }
3153
+ runtime = new Runtime(this.sourceFs, this.commands, this.middleware, this.budget, signal, this.fileWrites, this.outputFiles, signal, this.cancellation, this.cancellationState, this.cancellationOwner, this.cancellationDepth, this.cancellationMaxDepth);
3154
+ const processId = (childIdentities.get(this.budget) ?? 1000) + 1;
3155
+ if (!Number.isSafeInteger(processId))
3156
+ throw new RangeError("Shell child identity exhausted");
3157
+ childIdentities.set(this.budget, processId);
3158
+ const childScript = { ...script, lists: [{ pipelines: list.pipelines, operators: list.operators }] };
3159
+ return Object.freeze({ processId, run: () => {
3160
+ if (closed || execution)
3161
+ return Promise.reject(new TypeError("Prepared shell child run is single-use"));
3162
+ execution = Promise.resolve().then(async () => {
3163
+ signal.throwIfAborted();
3164
+ try {
3165
+ return await runtime.run(childScript, child, childIO);
3166
+ }
3167
+ catch (reason) {
3168
+ if (reason instanceof Flow && reason.kind === "return")
3169
+ return runtime.finishShell(child, childIO, reason.status);
3170
+ if (reason instanceof NounsetDiagnosticFailure)
3171
+ throw reason.reason;
3172
+ throw reason;
3173
+ }
3174
+ });
3175
+ return execution;
3176
+ } });
3177
+ }
3178
+ catch (reason) {
3179
+ ready();
3180
+ try {
3181
+ await close();
3182
+ }
3183
+ catch {
3184
+ this.commandSignal.throwIfAborted();
3185
+ }
3186
+ throw reason;
3187
+ }
3188
+ finally {
3189
+ ready();
3190
+ }
3191
+ }
2027
3192
  async pipeline(pipeline, state, io) {
2028
3193
  this.signal.throwIfAborted();
3194
+ const terminal = io.terminal?.target === pipeline ? io.terminal : undefined;
2029
3195
  let status;
2030
3196
  if (pipeline.commands.length === 1)
2031
- status = await this.command(pipeline.commands[0], state, io, false, pipeline.negate);
3197
+ status = await this.command(pipeline.commands[0], state, terminal ? { ...io, terminal: { target: pipeline.commands[0], frame: terminal.frame } } : io, false, pipeline.negate);
2032
3198
  else {
2033
3199
  const release = this.budget.reservePipelineStages(pipeline.commands.length);
2034
3200
  const retained = new Set();
@@ -2048,123 +3214,265 @@ export class Runtime {
2048
3214
  const completed = new Set();
2049
3215
  const closing = new Set();
2050
3216
  let statuses;
2051
- const stageLeases = [];
2052
3217
  try {
3218
+ if (state.extensions && !state.extensions.eventDepth) {
3219
+ for (const command of pipeline.commands) {
3220
+ if (command.kind === "simple" || command.kind === "arithmetic" || command.kind === "conditional") {
3221
+ this.writeVariable(state, "BASH_COMMAND", commandSpelling(command));
3222
+ }
3223
+ }
3224
+ }
2053
3225
  for (let index = 1; index < pipeline.commands.length; index++)
2054
3226
  pipes.push(createBytePipe({
2055
3227
  highWaterMark: this.budget.limits.pipeHighWaterMark, signal: this.signal,
2056
3228
  }));
2057
3229
  for (let index = 0; index < pipeline.commands.length; index++)
2058
3230
  controllers.push(new AbortController());
2059
- stageLeases.push(...pipeline.commands.map(() => (io[backgroundResources] ?? []).filter(resource => resource.destination).map(resource => ({ resource, release: resource.retain() }))));
2060
- io[backgroundPrune]?.(new Map());
3231
+ let transferred;
3232
+ let pendingTransfers = pipeline.commands.length;
3233
+ const transfer = new Promise(resolve => { transferred = resolve; });
3234
+ const retireBaseline = terminal ? transfer.then(() => terminal.frame.reconcile(new Map())) : Promise.resolve();
3235
+ retain(retireBaseline);
3236
+ void retireBaseline.catch(() => undefined);
3237
+ let preparedStages = 0;
3238
+ let acceptPreparation;
3239
+ let rejectPreparation;
3240
+ const installation = state.extensions?.checkpoints.length ? new Promise((resolve, reject) => {
3241
+ acceptPreparation = () => { if (++preparedStages === pipeline.commands.length)
3242
+ resolve(); };
3243
+ rejectPreparation = reject;
3244
+ }).then(async () => {
3245
+ await retireBaseline;
3246
+ await this.extensionCheckpoint("child-job-install", state, io);
3247
+ }) : undefined;
3248
+ if (installation) {
3249
+ retain(installation);
3250
+ void installation.catch(() => undefined);
3251
+ }
2061
3252
  const tasks = pipeline.commands.map(async (command, index) => {
2062
- let initialRedirection = true;
2063
- const inheritedLeases = stageLeases[index];
2064
- const incoming = pipes[index - 1];
2065
- const outgoing = pipes[index];
2066
- const childDepth = this.cancellationDepth + 1;
2067
- const controls = [
2068
- { role: "pipeline-control", signal: controllers[index].signal },
2069
- ];
2070
- const prepared = prepareChildCancellation(this.cancellation, undefined, this.cancellationAdmission(childDepth, controls.length), controls);
2071
- const owner = new InvocationCancellationOwner(io[invocationScope], prepared, this.cancellationState);
2072
- let boundary;
3253
+ let admitted = false;
3254
+ const admit = () => {
3255
+ if (admitted)
3256
+ return;
3257
+ admitted = true;
3258
+ if (--pendingTransfers === 0)
3259
+ transferred();
3260
+ };
2073
3261
  try {
2074
- boundary = owner.activate();
2075
- }
2076
- catch (error) {
2077
- await owner.abandon(Promise.resolve());
2078
- throw error;
2079
- }
2080
- const signal = AbortSignal.any([boundary.deliverySignal, io[invocationScope].signal]);
2081
- const frame = {};
2082
- const runtime = new Runtime(this.fs, this.commands, this.middleware, this.budget, signal, this.fileWrites, this.outputFiles, boundary.deliverySignal, boundary, this.cancellationState, owner, childDepth, this.cancellationMaxDepth, frame, this.inputProfile);
2083
- const input = new ShellInput(incoming?.readable ?? io.stdin, this.budget, signal);
2084
- const pipeOutput = outgoing && { [outputFailure]: outgoing.abort, ownedOutput: outgoing.writable.ownedOutput, write: async (chunk) => {
2085
- try {
2086
- await outgoing.writable.write(chunk);
2087
- if (chunk.byteLength)
2088
- written.add(index);
2089
- }
2090
- catch (error) {
2091
- if (errorCode(error) === "EPIPE") {
2092
- const closed = new PipelineClosed();
2093
- controllers[index].abort(closed);
2094
- throw closed;
2095
- }
2096
- throw error;
2097
- }
2098
- } };
2099
- const resources = new BackgroundResources();
2100
- resources.destination = backgroundDestination(pipeOutput ?? io.stdout);
2101
- const executeStage = async () => {
3262
+ const incoming = pipes[index - 1];
3263
+ const outgoing = pipes[index];
3264
+ const childDepth = this.cancellationDepth + 1;
3265
+ const controls = [
3266
+ { role: "pipeline-control", signal: controllers[index].signal },
3267
+ ];
3268
+ const prepared = prepareChildCancellation(this.cancellation, undefined, this.cancellationAdmission(childDepth, controls.length), controls);
3269
+ const owner = new InvocationCancellationOwner(io[invocationScope], prepared, this.cancellationState);
3270
+ let boundary;
2102
3271
  try {
2103
- let exitCode;
2104
- try {
2105
- const child = await cloneState(state, this.signal);
2106
- child.isolated = true;
2107
- const work = runtime.runCommandIsolated(command, child, {
2108
- ...isolateIO(io),
2109
- [backgroundResources]: [...io[backgroundResources] ?? [], resources],
2110
- [backgroundPrune]: descriptors => {
2111
- if (!initialRedirection)
2112
- return;
2113
- initialRedirection = false;
2114
- const destinations = new Set([...descriptors.values()].flatMap(descriptor => descriptor.output && !descriptor.closed ? [backgroundDestination(descriptor.output)] : []));
2115
- for (const lease of inheritedLeases)
2116
- if (!destinations.has(lease.resource.destination))
2117
- lease.release();
2118
- },
2119
- stdin: input,
2120
- ...(incoming ? { stdinIsDefault: false } : {}),
2121
- stdout: pipeOutput ? this.budget.sink(pipeOutput, signal) : signalSink(io.stdout, signal),
2122
- stderr: signalSink(io.stderr, signal),
2123
- }).finally(() => stateMonitor(child)?.closeValues());
2124
- retain(work);
2125
- exitCode = await interruptible(work, signal);
2126
- if (resources.busy)
2127
- await interruptible(resources.drain(), signal);
2128
- }
2129
- catch (error) {
2130
- if (!(error instanceof PipelineClosed))
2131
- throw error;
2132
- exitCode = 141;
2133
- }
2134
- return { exitCode };
3272
+ boundary = owner.activate();
2135
3273
  }
2136
- finally {
2137
- inheritedLeases.forEach(lease => lease.release());
2138
- completed.add(index);
2139
- if (incoming) {
2140
- const upstream = index - 1;
2141
- const close = scheduleTurn(() => {
2142
- closing.delete(close);
2143
- if (written.has(upstream) && !completed.has(upstream))
2144
- controllers[upstream].abort(new PipelineClosed());
2145
- });
2146
- closing.add(close);
2147
- await incoming.abort();
2148
- }
2149
- await input.close().catch((error) => { if (!(error instanceof PipelineClosed))
2150
- throw error; });
2151
- if (outgoing)
2152
- await outgoing.close().catch(() => undefined);
3274
+ catch (error) {
3275
+ await owner.abandon(Promise.resolve());
3276
+ throw error;
2153
3277
  }
2154
- };
2155
- let captured;
2156
- try {
2157
- captured = { kind: "return", value: await executeStage() };
3278
+ const signal = AbortSignal.any([boundary.deliverySignal, io[invocationScope].signal]);
3279
+ const frame = {};
3280
+ const runtime = new Runtime(this.fs, this.commands, this.middleware, this.budget, signal, this.fileWrites, this.outputFiles, boundary.deliverySignal, boundary, this.cancellationState, owner, childDepth, this.cancellationMaxDepth, frame, this.inputProfile);
3281
+ let captured;
3282
+ const preparationCleanup = [];
3283
+ let stageOwnsCleanup = false;
3284
+ try {
3285
+ const references = new PipeDescriptorFrame(io[invocationScope]);
3286
+ preparationCleanup.push(() => references.close());
3287
+ const descriptorFrame = new PreparedDescriptorFrame(references, this.budget);
3288
+ preparationCleanup.push(() => descriptorFrame.close());
3289
+ const reading = incoming?.endpoints?.read;
3290
+ const writing = outgoing?.endpoints?.write;
3291
+ const readReference = reading && references.open(reading, this.budget);
3292
+ const writeReference = writing && references.open(writing, this.budget);
3293
+ const input = incoming
3294
+ ? new ShellInput(reading?.readable ?? incoming.readable, this.budget, signal, { provenance: "stream", poll: () => incoming.readiness() })
3295
+ : new ShellInput(io.stdin, this.budget, signal);
3296
+ preparationCleanup.push(() => input.close());
3297
+ const writable = writing?.writable ?? outgoing?.writable;
3298
+ const failOutput = writable?.[outputFailure]?.bind(writable);
3299
+ const pipeOutput = outgoing && { ...(failOutput ? { [outputFailure]: failOutput } : {}), ownedOutput: writable.ownedOutput, write: async (chunk) => {
3300
+ try {
3301
+ await writable.write(chunk);
3302
+ if (chunk.byteLength)
3303
+ written.add(index);
3304
+ }
3305
+ catch (error) {
3306
+ if (errorCode(error) === "EPIPE") {
3307
+ const closed = new PipelineClosed();
3308
+ controllers[index].abort(closed);
3309
+ throw closed;
3310
+ }
3311
+ throw error;
3312
+ }
3313
+ } };
3314
+ const executeStage = async () => {
3315
+ let preparedChild;
3316
+ let preparationFailed = false;
3317
+ let started = false;
3318
+ let checkpointFailure;
3319
+ let outcome;
3320
+ try {
3321
+ let exitCode;
3322
+ try {
3323
+ const child = await cloneState(state, this.signal);
3324
+ preparedChild = child;
3325
+ child.extensions = undefined;
3326
+ child.extensions = forkExtensions(state.extensions, "pipeline");
3327
+ child.isolated = true;
3328
+ const inherited = isolateIO(io, references);
3329
+ const childIO = {
3330
+ ...inherited,
3331
+ stdin: input,
3332
+ ...(incoming ? { asyncDefaultInput: undefined } : {}),
3333
+ ...(incoming ? { stdinIsDefault: false } : {}),
3334
+ stdout: pipeOutput ? this.budget.sink(pipeOutput, signal) : signalSink(io.stdout, signal),
3335
+ stderr: signalSink(io.stderr, signal),
3336
+ terminal: { target: command, frame: descriptorFrame },
3337
+ };
3338
+ const descriptors = new Map(childIO.descriptors);
3339
+ descriptors.set(0, incoming ? { input, stdinIsDefault: false, ...(readReference ? { pipe: readReference } : {}) } : { ...descriptors.get(0), input });
3340
+ descriptors.set(1, outgoing ? { output: childIO.stdout, ...(writeReference ? { pipe: writeReference } : {}) } : { ...descriptors.get(1), output: childIO.stdout });
3341
+ descriptors.set(2, { ...descriptors.get(2), output: childIO.stderr });
3342
+ childIO.descriptors = descriptors;
3343
+ descriptorFrame.acquire(descriptors);
3344
+ admit();
3345
+ acceptPreparation?.();
3346
+ await retireBaseline;
3347
+ if (installation) {
3348
+ await installation;
3349
+ signal.throwIfAborted();
3350
+ }
3351
+ const execution = runtime.runCommandIsolated(command, child, childIO).then(status => runtime.finishShell(child, childIO, status));
3352
+ const work = (child.extensions?.checkpoints.length ? execution.catch(reason => {
3353
+ if (reason instanceof ExtensionCheckpointFailure)
3354
+ checkpointFailure = reason;
3355
+ throw reason;
3356
+ }) : execution).finally(async () => {
3357
+ try {
3358
+ await runtime.releaseExtensions(child);
3359
+ }
3360
+ catch (cleanup) {
3361
+ if (checkpointFailure)
3362
+ io[invocationScope].failures.push(cleanup);
3363
+ else
3364
+ throw cleanup;
3365
+ }
3366
+ finally {
3367
+ stateMonitor(child)?.closeValues();
3368
+ }
3369
+ });
3370
+ preparedChild = undefined;
3371
+ started = true;
3372
+ retain(work);
3373
+ exitCode = await interruptible(work, signal);
3374
+ }
3375
+ catch (error) {
3376
+ if (error instanceof ExtensionCheckpointFailure)
3377
+ checkpointFailure = error;
3378
+ if (!started) {
3379
+ preparationFailed = true;
3380
+ rejectPreparation?.(error);
3381
+ }
3382
+ if (!(error instanceof PipelineClosed))
3383
+ throw error;
3384
+ exitCode = 141;
3385
+ }
3386
+ outcome = { kind: "return", value: { exitCode } };
3387
+ }
3388
+ catch (reason) {
3389
+ outcome = { kind: "throw", reason };
3390
+ }
3391
+ finally {
3392
+ admit();
3393
+ const cleanupFailures = [];
3394
+ if (preparedChild) {
3395
+ const child = preparedChild;
3396
+ await io[invocationScope].cleanup(async () => {
3397
+ try {
3398
+ await runtime.releaseExtensions(child);
3399
+ }
3400
+ finally {
3401
+ stateMonitor(child)?.closeValues();
3402
+ }
3403
+ });
3404
+ }
3405
+ completed.add(index);
3406
+ if (incoming && !reading) {
3407
+ const upstream = index - 1;
3408
+ const close = scheduleTurn(() => {
3409
+ closing.delete(close);
3410
+ if (written.has(upstream) && !completed.has(upstream))
3411
+ controllers[upstream].abort(new PipelineClosed());
3412
+ });
3413
+ closing.add(close);
3414
+ try {
3415
+ await incoming.abort();
3416
+ }
3417
+ catch (reason) {
3418
+ cleanupFailures.push(reason);
3419
+ }
3420
+ }
3421
+ try {
3422
+ await input.close().catch((error) => { if (!(error instanceof PipelineClosed))
3423
+ throw error; });
3424
+ }
3425
+ catch (reason) {
3426
+ cleanupFailures.push(reason);
3427
+ }
3428
+ try {
3429
+ await descriptorFrame.close();
3430
+ }
3431
+ catch (reason) {
3432
+ cleanupFailures.push(reason);
3433
+ }
3434
+ try {
3435
+ await references.close();
3436
+ }
3437
+ catch (reason) {
3438
+ cleanupFailures.push(reason);
3439
+ }
3440
+ if (outgoing && !writing)
3441
+ await outgoing.close().catch(() => undefined);
3442
+ if (checkpointFailure || installation && preparationFailed)
3443
+ io[invocationScope].failures.push(...cleanupFailures);
3444
+ else if (cleanupFailures.length) {
3445
+ io[invocationScope].failures.push(...cleanupFailures.slice(1));
3446
+ outcome = { kind: "throw", reason: cleanupFailures[0] };
3447
+ }
3448
+ }
3449
+ if (outcome.kind === "throw")
3450
+ throw outcome.reason;
3451
+ return outcome.value;
3452
+ };
3453
+ stageOwnsCleanup = true;
3454
+ captured = { kind: "return", value: await executeStage() };
3455
+ }
3456
+ catch (reason) {
3457
+ rejectPreparation?.(reason);
3458
+ if (!stageOwnsCleanup)
3459
+ await Promise.all(preparationCleanup.map(cleanup => io[invocationScope].cleanup(cleanup)));
3460
+ captured = frame.report && Object.is(frame.report.origin.signal.reason, reason)
3461
+ ? { kind: "throw", reason, report: frame.report }
3462
+ : { kind: "throw", reason };
3463
+ }
3464
+ const selection = await owner.finish(Promise.resolve(), captured);
3465
+ if (selection.outcome.kind === "throw")
3466
+ throw selection.outcome.reason;
3467
+ return selection.outcome.value.exitCode;
2158
3468
  }
2159
3469
  catch (reason) {
2160
- captured = frame.report && Object.is(frame.report.origin.signal.reason, reason)
2161
- ? { kind: "throw", reason, report: frame.report }
2162
- : { kind: "throw", reason };
2163
- }
2164
- const selection = await owner.finish(Promise.resolve(), captured);
2165
- if (selection.outcome.kind === "throw")
2166
- throw selection.outcome.reason;
2167
- return selection.outcome.value.exitCode;
3470
+ rejectPreparation?.(reason);
3471
+ throw reason;
3472
+ }
3473
+ finally {
3474
+ admit();
3475
+ }
2168
3476
  });
2169
3477
  for (const task of tasks)
2170
3478
  retain(task);
@@ -2172,9 +3480,6 @@ export class Runtime {
2172
3480
  }
2173
3481
  finally {
2174
3482
  try {
2175
- for (const leases of stageLeases)
2176
- for (const lease of leases)
2177
- lease.release();
2178
3483
  for (const close of closing)
2179
3484
  cancelTurn(close);
2180
3485
  for (const [index, controller] of controllers.entries())
@@ -2195,25 +3500,31 @@ export class Runtime {
2195
3500
  status = state.pipefail ? statuses.findLast((status) => status !== 0) ?? 0 : statuses.at(-1);
2196
3501
  }
2197
3502
  if (pipeline.commands.length > 1)
2198
- this.errexit(status, state, io);
2199
- return pipeline.negate ? Number(status === 0) : status;
3503
+ await this.errexit(status, state, io);
3504
+ return pipeline.negate && !terminal ? Number(status === 0) : status;
2200
3505
  }
2201
3506
  async runCommandIsolated(command, state, io, fileShortcut = false) {
2202
3507
  try {
3508
+ await this.startExtensions(state, io);
2203
3509
  return await this.command(command, state, io, fileShortcut);
2204
3510
  }
2205
3511
  catch (error) {
2206
3512
  if (error instanceof NounsetDiagnosticFailure)
2207
3513
  throw error;
2208
- if (error instanceof Flow && (error.kind === "exit" || error.kind === "return"))
3514
+ if (error instanceof Flow && (error.kind === "exit" || error.kind === "discard"))
3515
+ return this.finishShell(state, io, error.status);
3516
+ if (error instanceof Flow && error.kind === "return")
2209
3517
  return error.status;
2210
3518
  throw error;
2211
3519
  }
2212
3520
  }
2213
- errexit(status, state, io) {
3521
+ async errexit(status, state, io) {
2214
3522
  this.signal.throwIfAborted();
2215
- if (status !== 0 && state.errexit && !io.execution?.ignoreErrexit)
2216
- throw new Flow("exit", status);
3523
+ if (status !== 0 && !io.execution?.ignoreErrexit) {
3524
+ await this.extensionEvent("error", state, io, status);
3525
+ if (state.errexit)
3526
+ throw new Flow("exit", status);
3527
+ }
2217
3528
  }
2218
3529
  async publishStatus(state, statuses, io) {
2219
3530
  this.signal.throwIfAborted();
@@ -2227,7 +3538,25 @@ export class Runtime {
2227
3538
  }
2228
3539
  async command(command, state, io, fileShortcut = false, publicationNegate = false) {
2229
3540
  io[invocationScope].assertOpen();
3541
+ if (state.extensions && (command.kind === "simple" || command.kind === "arithmetic" || command.kind === "conditional")) {
3542
+ io = { ...io, diagnosticLine: io.diagnosticCommandLines?.get(command) ?? (command.line ?? 1) + (io.diagnosticOffset ?? 0) };
3543
+ const description = commandSpelling(command);
3544
+ if (!state.extensions.eventDepth)
3545
+ this.writeVariable(state, "BASH_COMMAND", description);
3546
+ if (await this.extensionEvent("command", state, io, state.status, description))
3547
+ return state.status;
3548
+ }
2230
3549
  const publishes = command.kind === "simple" || command.kind === "subshell" || command.kind === "arithmetic" || command.kind === "conditional";
3550
+ const terminal = io.terminal?.target === command ? io.terminal : undefined;
3551
+ if (terminal)
3552
+ terminal.beforeExit = async (status, completionIO) => {
3553
+ if (publishes) {
3554
+ const reported = publicationNegate && (command.kind === "conditional" || command.kind === "arithmetic") ? Number(status === 0) : status;
3555
+ await this.publishStatus(state, [reported], completionIO);
3556
+ terminal.published = status;
3557
+ await this.errexit(status, state, completionIO);
3558
+ }
3559
+ };
2231
3560
  let status;
2232
3561
  try {
2233
3562
  status = await io[invocationScope].run(() => this.executeCommand(command, state, io, fileShortcut));
@@ -2241,44 +3570,60 @@ export class Runtime {
2241
3570
  }
2242
3571
  throw error;
2243
3572
  }
3573
+ if (terminal && state.extensions?.exitStatus !== undefined)
3574
+ state.extensions.exitStatus = status;
2244
3575
  if (publishes) {
2245
3576
  const reported = publicationNegate && (command.kind === "conditional" || command.kind === "arithmetic") ? Number(status === 0) : status;
2246
- await this.publishStatus(state, [reported], io);
2247
- this.errexit(status, state, io);
3577
+ if (terminal?.published !== status)
3578
+ await this.publishStatus(state, [reported], io);
3579
+ if (!terminal?.completed)
3580
+ await this.errexit(status, state, io);
2248
3581
  }
2249
3582
  return status;
2250
3583
  }
2251
3584
  async executeCommand(command, state, originalIO, fileShortcut = false) {
2252
- state = trackState(state, this.budget, originalIO[invocationScope]);
2253
- originalIO = activeIO(originalIO);
2254
3585
  originalIO.descriptors ??= new Map([
2255
3586
  [0, { input: originalIO.stdin, ...(originalIO.stdinIsDefault === undefined ? {} : { stdinIsDefault: originalIO.stdinIsDefault }) }],
2256
3587
  [1, { output: originalIO.stdout }], [2, { output: originalIO.stderr }],
2257
3588
  ]);
2258
- const diagnosticLine = (command.line ?? 1) + (originalIO.diagnosticOffset ?? 0);
3589
+ const terminal = originalIO.terminal?.target === command ? originalIO.terminal : undefined;
3590
+ originalIO = { ...originalIO, terminal: undefined };
3591
+ state = trackState(state, this.budget, originalIO[invocationScope]);
3592
+ if (originalIO.asyncDefaultInput) {
3593
+ if (!command.redirects.some(redirect => redirect.descriptor === 0)) {
3594
+ const descriptors = new Map(originalIO.descriptors);
3595
+ descriptors.set(0, { input: originalIO.asyncDefaultInput, stdinIsDefault: true });
3596
+ originalIO = { ...originalIO, descriptors, stdin: originalIO.asyncDefaultInput, stdinIsDefault: true, asyncDefaultInput: undefined };
3597
+ }
3598
+ else
3599
+ originalIO = { ...originalIO, asyncDefaultInput: undefined };
3600
+ }
3601
+ originalIO = activeIO(originalIO);
3602
+ const diagnosticLine = originalIO.diagnosticCommandLines?.get(command) ?? (command.line ?? 1) + (originalIO.diagnosticOffset ?? 0);
2259
3603
  originalIO = { ...originalIO, diagnosticLine, substitutionDiagnosticLine: originalIO.substitutionDiagnosticLines?.get(command) ?? diagnosticLine };
3604
+ const references = new PipeDescriptorFrame(originalIO[invocationScope]);
2260
3605
  if (command.kind === "subshell")
2261
- originalIO = isolateIO(originalIO);
3606
+ originalIO = isolateIO(originalIO, references);
2262
3607
  this.budget.tick();
2263
3608
  if (this.budget.commands % 128 === 0)
2264
3609
  await yieldTurn(this.signal);
2265
3610
  this.signal.throwIfAborted();
2266
- const resources = new BackgroundResources();
2267
- originalIO = { ...originalIO, [backgroundResources]: [...originalIO[backgroundResources] ?? [], resources] };
2268
3611
  const inputs = new Set();
2269
3612
  const outputs = new Set();
3613
+ const outputFailures = [];
3614
+ let outputStatus;
2270
3615
  const finishOutputs = async (status) => {
2271
3616
  const pending = [...outputs];
2272
3617
  outputs.clear();
2273
- const finish = async () => {
2274
- await resources.drain();
2275
- const settled = await Promise.allSettled(pending.map(close => close({ status })));
2276
- throwCleanupFailures(settled.filter(result => result.status === "rejected").map(result => result.reason));
2277
- };
2278
- if (resources.busy)
2279
- state.backgroundJobs.execution.track(finish());
2280
- else
2281
- await finish();
3618
+ const settled = await Promise.allSettled(pending.map(close => close({ status })));
3619
+ for (const [index, result] of settled.entries()) {
3620
+ const descriptor = pending[index]?.descriptor;
3621
+ if (result.status === "rejected" && descriptor)
3622
+ outputFailures.push({ descriptor, reason: result.reason });
3623
+ }
3624
+ if (status !== 0)
3625
+ outputStatus = status;
3626
+ throwCleanupFailures(settled.filter(result => result.status === "rejected").map(result => result.reason));
2282
3627
  };
2283
3628
  const allocation = this.budget.values.scope();
2284
3629
  originalIO = { ...originalIO, [valueScope]: allocation };
@@ -2289,21 +3634,37 @@ export class Runtime {
2289
3634
  try {
2290
3635
  const execute = async () => {
2291
3636
  if (command.kind === "function") {
2292
- if (state.profile === "sh" && specialBuiltinNames.has(command.name)) {
3637
+ if (state.profile === "sh" && (specialBuiltinNames.has(command.name) || state.extensions?.builtins.get(command.name)?.special)) {
2293
3638
  await this.diagnostic(io, `\`${command.name}': is a special builtin`);
2294
3639
  throw new Flow("exit", 2);
2295
3640
  }
2296
- state.functions.set(command.name, { ...command.body, sourceName: io.scriptName ?? "shell" });
3641
+ const body = { ...command.body, sourceName: io.scriptName ?? "shell" };
3642
+ const offset = io.diagnosticOffset ?? 0;
3643
+ const bodyLine = io.functionCommandLines?.get(command.body);
3644
+ if (bodyLine !== undefined)
3645
+ body.line = bodyLine - offset;
3646
+ functionDiagnostics.set(body, { offset, ...(bodyLine === undefined ? {} : { lines: io.functionCommandLines }) });
3647
+ state.functions.set(command.name, body);
2297
3648
  return 0;
2298
3649
  }
2299
- if (command.kind === "simple")
2300
- return await this.simple(command, state, originalIO, inputs, outputs, () => {
3650
+ if (command.kind === "simple") {
3651
+ const status = await this.simple(command, state, originalIO, inputs, outputs, () => {
2301
3652
  snapshotScope = originalIO[invocationScope].child();
2302
3653
  void snapshotScope.run(() => new Promise(resolve => { finishSnapshot = resolve; }));
2303
3654
  return snapshotScope;
2304
- }, fileShortcut);
3655
+ }, fileShortcut, terminal);
3656
+ if (originalIO.assignmentDiagnosticContext)
3657
+ originalIO.assignmentDiagnosticContext.name = undefined;
3658
+ return status;
3659
+ }
2305
3660
  io = await this.redirect(command.redirects, state, io, inputs, outputs, command.kind === "subshell", command.kind !== "subshell");
3661
+ if (terminal) {
3662
+ terminal.io = io;
3663
+ await terminal.frame.reconcile(io.descriptors);
3664
+ }
2306
3665
  if (command.kind === "conditional") {
3666
+ if (io.assignmentDiagnosticContext)
3667
+ io.assignmentDiagnosticContext.name = "[[";
2307
3668
  try {
2308
3669
  return await evaluateConditional(command.expression, {
2309
3670
  fs: this.fs, cwd: state.cwd, signal: this.signal,
@@ -2311,7 +3672,7 @@ export class Runtime {
2311
3672
  work: { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation },
2312
3673
  expand: async (word, pattern = false) => (await this.word(word, state, { ...io, nameExpansionContext: "conditional" }, false, pattern, false, pattern)).join(""),
2313
3674
  regex: (subject, pattern) => this.ere(subject, pattern, state, { ...io, nameExpansionContext: "conditional" }),
2314
- option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
3675
+ option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : state.extensions?.options.get(name)?.enabled ?? false,
2315
3676
  present: name => {
2316
3677
  const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(0|[1-9][0-9]*|[@*])\])?$/u.exec(name);
2317
3678
  if (!match)
@@ -2320,7 +3681,7 @@ export class Runtime {
2320
3681
  const selector = match[2];
2321
3682
  if (selector === "@" || selector === "*")
2322
3683
  return binding ? binding.values.size > 0 : this.variable(state, match[1]) !== undefined;
2323
- const index = selector === undefined ? 0 : numericIndex({ decimal: selector });
3684
+ const index = selector === undefined ? 0 : numericIndex({ decimal: selector }, 4294967295);
2324
3685
  if (index === undefined)
2325
3686
  throw new ConditionalUnsupported("[[ variable index: unsupported conditional profile");
2326
3687
  return binding ? binding.get(index) !== undefined : index === 0 && this.variable(state, match[1]) !== undefined;
@@ -2366,6 +3727,8 @@ export class Runtime {
2366
3727
  }
2367
3728
  }
2368
3729
  if (command.kind === "arithmetic") {
3730
+ if (io.assignmentDiagnosticContext)
3731
+ io.assignmentDiagnosticContext.name = "((";
2369
3732
  try {
2370
3733
  return Number(this.arithmeticValue(command.expression, state, io) === 0n);
2371
3734
  }
@@ -2376,13 +3739,25 @@ export class Runtime {
2376
3739
  }
2377
3740
  if (command.kind === "subshell") {
2378
3741
  const child = await cloneState(state, this.signal);
2379
- child.isolated = true;
2380
- child.loopDepth = 0;
3742
+ child.extensions = undefined;
3743
+ let started = false;
2381
3744
  try {
3745
+ child.extensions = forkExtensions(state.extensions, "subshell");
3746
+ child.isolated = true;
3747
+ child.loopDepth = 0;
3748
+ if (state.extensions?.checkpoints.length)
3749
+ await this.extensionCheckpoint("child-job-install", state, io);
3750
+ started = true;
2382
3751
  return await this.run(command.body, child, io);
2383
3752
  }
2384
3753
  finally {
2385
- stateMonitor(child)?.closeValues();
3754
+ try {
3755
+ if (!started && child.extensions?.cleanup.length)
3756
+ await io[invocationScope].cleanup(() => this.releaseExtensions(child));
3757
+ }
3758
+ finally {
3759
+ stateMonitor(child)?.closeValues();
3760
+ }
2386
3761
  }
2387
3762
  }
2388
3763
  if (command.kind === "group")
@@ -2395,6 +3770,13 @@ export class Runtime {
2395
3770
  return command.otherwise ? await this.script(command.otherwise, state, io) : 0;
2396
3771
  }
2397
3772
  if (command.kind === "case") {
3773
+ if (state.extensions) {
3774
+ const description = `case ${command.subject.spelling ?? command.subject.plain ?? ""} in `;
3775
+ if (!state.extensions.eventDepth)
3776
+ this.writeVariable(state, "BASH_COMMAND", description);
3777
+ if (await this.extensionEvent("command", state, io, state.status, description))
3778
+ return state.status;
3779
+ }
2398
3780
  const subject = (await this.word(command.subject, state, io, false)).join("");
2399
3781
  const work = { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation };
2400
3782
  let status = 0;
@@ -2430,7 +3812,16 @@ export class Runtime {
2430
3812
  const values = command.words ? await this.valueWords(command.words, state, io) : this.positionalValues(state);
2431
3813
  for (const value of values) {
2432
3814
  this.budget.loop();
3815
+ if (io.assignmentDiagnosticContext)
3816
+ io.assignmentDiagnosticContext.name = undefined;
2433
3817
  await this.assignVariable(state, command.name, value);
3818
+ if (state.extensions) {
3819
+ const description = `for ${command.name} in ${command.words?.map(word => word.spelling ?? word.plain ?? "").join(" ") ?? '"$@"'}`;
3820
+ if (!state.extensions.eventDepth)
3821
+ this.writeVariable(state, "BASH_COMMAND", description);
3822
+ if (await this.extensionEvent("command", state, io, state.status, description))
3823
+ continue;
3824
+ }
2434
3825
  const result = await this.loopBody(command.body, state, io);
2435
3826
  status = result.status;
2436
3827
  if (result.stop)
@@ -2553,7 +3944,12 @@ export class Runtime {
2553
3944
  }
2554
3945
  return status;
2555
3946
  };
2556
- const status = await execute();
3947
+ let status = await execute();
3948
+ if (terminal) {
3949
+ await terminal.beforeExit?.(status, terminal.io ?? io);
3950
+ status = await this.finishShell(state, terminal.io ?? io, status);
3951
+ terminal.completed = true;
3952
+ }
2557
3953
  await finishOutputs(status);
2558
3954
  return status;
2559
3955
  }
@@ -2565,20 +3961,37 @@ export class Runtime {
2565
3961
  if (error instanceof NounsetDiagnosticFailure)
2566
3962
  diagnosticFailure = error;
2567
3963
  this.signal.throwIfAborted();
2568
- if (error instanceof Flow)
2569
- await finishOutputs(error.status);
3964
+ if (error instanceof Flow) {
3965
+ if (terminal && error.kind === "exit" && !(error instanceof NounsetDiagnosticFailure)) {
3966
+ await this.finishShell(state, terminal.io ?? io, error.status);
3967
+ terminal.completed = true;
3968
+ }
3969
+ try {
3970
+ await finishOutputs(error.status);
3971
+ }
3972
+ catch (reason) {
3973
+ this.signal.throwIfAborted();
3974
+ if (!outputFailures.length)
3975
+ throw reason;
3976
+ await writeText(io.stderr, `${io.scriptName ?? "shell"}: line ${io.diagnosticLine ?? 1}: ${message(reason, this.budget.onInternalError)}\n`);
3977
+ for (const failure of outputFailures)
3978
+ failure.descriptor.acknowledgeCloseFailure(failure.reason);
3979
+ }
3980
+ }
2570
3981
  if (error instanceof Flow || error instanceof ShellLimitError || error instanceof ShellSyntaxError)
2571
3982
  throw error;
2572
3983
  this.clearOutcomeReport();
2573
3984
  if (error instanceof HereDocumentSyntaxError) {
2574
3985
  await writeDiagnostic(io.stderr, error.diagnostic);
2575
3986
  if (command.kind !== "simple" && command.kind !== "subshell" && command.kind !== "arithmetic" && command.kind !== "conditional")
2576
- this.errexit(1, state, io);
3987
+ await this.errexit(1, state, io);
2577
3988
  return 1;
2578
3989
  }
2579
3990
  if (errorCode(error) === "EPIPE") {
3991
+ for (const failure of outputFailures)
3992
+ failure.descriptor.acknowledgeCloseFailure(failure.reason);
2580
3993
  if (command.kind !== "simple" && command.kind !== "subshell" && command.kind !== "arithmetic" && command.kind !== "conditional")
2581
- this.errexit(141, state, io);
3994
+ await this.errexit(141, state, io);
2582
3995
  return 141;
2583
3996
  }
2584
3997
  const publicMessage = message(error, this.budget.onInternalError);
@@ -2595,13 +4008,18 @@ export class Runtime {
2595
4008
  diagnosticFailure = new NounsetDiagnosticFailure(reason);
2596
4009
  throw diagnosticFailure;
2597
4010
  }
2598
- throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1);
4011
+ throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1, "exit", 1, undefined, true);
2599
4012
  }
2600
4013
  if (error instanceof ArrayFailure)
2601
4014
  await writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: line ${line}: ${diagnostic ?? publicMessage}\n`);
2602
4015
  else {
2603
4016
  try {
2604
- await writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: line ${line}: ${diagnostic ?? publicMessage}\n`);
4017
+ const origin = io.assignmentDiagnosticContext;
4018
+ const namedDeclaration = error instanceof DiscardCommandFailure && error.origin === "declaration" && origin?.name !== undefined;
4019
+ const detail = diagnostic ?? `${namedDeclaration ? `${origin.name}: ` : ""}${publicMessage}`;
4020
+ await writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: line ${line}: ${detail}\n`);
4021
+ for (const failure of outputFailures)
4022
+ failure.descriptor.acknowledgeCloseFailure(failure.reason);
2605
4023
  }
2606
4024
  catch (failure) {
2607
4025
  this.signal.throwIfAborted();
@@ -2612,35 +4030,26 @@ export class Runtime {
2612
4030
  throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1);
2613
4031
  if (error instanceof FatalCommandFailure)
2614
4032
  throw completedExit(error.status);
2615
- const status = error instanceof CommandFailure ? error.status : 1;
4033
+ if (error instanceof DiscardCommandFailure)
4034
+ throw completedExit(error.status, "discard");
4035
+ const status = outputStatus ?? (error instanceof CommandFailure ? error.status : 1);
2616
4036
  if (command.kind !== "simple" && command.kind !== "subshell" && command.kind !== "arithmetic" && command.kind !== "conditional")
2617
- this.errexit(status, state, io);
4037
+ await this.errexit(status, state, io);
2618
4038
  return status;
2619
4039
  }
2620
4040
  finally {
2621
4041
  try {
2622
- const close = async () => {
2623
- await resources.drain();
2624
- await Promise.allSettled([
2625
- ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
2626
- ...[...inputs].map(async (input) => { try {
2627
- await input.close();
2628
- }
2629
- finally {
2630
- await inputScopes.get(input)?.close();
2631
- } }),
2632
- ]).then(results => {
2633
- const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
2634
- if (diagnosticFailure)
2635
- io[invocationScope].failures.push(...failures);
2636
- else
2637
- throwCleanupFailures(failures);
2638
- }).finally(() => allocation.close());
2639
- };
2640
- if (resources.busy)
2641
- state.backgroundJobs.execution.track(close());
2642
- else
2643
- await close();
4042
+ await Promise.allSettled([
4043
+ references.close(),
4044
+ ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
4045
+ ...[...inputs].map(async (input) => input.close()),
4046
+ ]).then(results => {
4047
+ const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
4048
+ if (diagnosticFailure)
4049
+ io[invocationScope].failures.push(...failures);
4050
+ else
4051
+ throwCleanupFailures(failures);
4052
+ }).finally(() => allocation.close());
2644
4053
  }
2645
4054
  finally {
2646
4055
  finishSnapshot?.();
@@ -2649,55 +4058,68 @@ export class Runtime {
2649
4058
  }
2650
4059
  }
2651
4060
  async loopBody(body, state, io) {
4061
+ let status = 0;
4062
+ let control;
2652
4063
  try {
2653
- return { status: await this.script(body, state, io), stop: false };
4064
+ status = await this.script(body, state, io);
2654
4065
  }
2655
4066
  catch (error) {
2656
4067
  if (!(error instanceof Flow) || (error.kind !== "break" && error.kind !== "continue"))
2657
4068
  throw error;
2658
- if (--error.levels > 0)
2659
- throw error;
2660
- return { status: 0, stop: error.kind === "break" };
4069
+ control = error;
2661
4070
  }
4071
+ if (state.extensions?.checkpoints.length)
4072
+ await this.extensionCheckpoint("loop-body-complete", state, io);
4073
+ if (control && --control.levels > 0)
4074
+ throw control;
4075
+ return { status, stop: control?.kind === "break" };
2662
4076
  }
2663
4077
  async document(document, state, io, line = document.endLine) {
2664
4078
  this.signal.throwIfAborted();
2665
4079
  let value = "";
2666
4080
  let fragments;
4081
+ const allocation = this.budget.values.scope();
2667
4082
  let size = 0;
2668
4083
  let words = 0;
2669
4084
  const warnings = [];
2670
4085
  try {
2671
- for (const word of hereDocumentWords(document, line, byteLocale(state.variables), warnings, this.budget.parsing)) {
4086
+ for (const word of hereDocumentWords(document, line, byteLocale(state.variables), warnings, this.budget.parsing, state.extensions?.syntax)) {
2672
4087
  this.signal.throwIfAborted();
2673
4088
  for (const warning of warnings.splice(0))
2674
4089
  await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
2675
4090
  if (++words % 128 === 0)
2676
4091
  await yieldTurn(this.signal);
2677
- const part = concatShellValues(await this.valueWord(word, state, { ...io, nameExpansionContext: "document" }, false), io[valueScope]);
2678
- size += shellValueByteLength(part);
2679
- if (size > this.budget.limits.maxExpansionBytes)
2680
- this.budget.fail("maxExpansionBytes");
2681
- if (!fragments && typeof part === "string")
2682
- value += part;
2683
- else {
2684
- if (!fragments) {
2685
- io[valueScope]?.reserve(64 + value.length * 2, 0);
2686
- fragments = [value];
2687
- value = "";
4092
+ for (const part of await this.valueWord(word, state, { ...io, nameExpansionContext: "document" }, false, false, false, false, undefined, true, false)) {
4093
+ size += shellValueByteLength(part);
4094
+ if (size > this.budget.limits.maxExpansionBytes)
4095
+ this.budget.fail("maxExpansionBytes");
4096
+ if (!fragments && typeof part === "string")
4097
+ value += part;
4098
+ else {
4099
+ if (!fragments) {
4100
+ allocation.reserve(64 + value.length * 2, 2);
4101
+ fragments = [value];
4102
+ value = "";
4103
+ }
4104
+ allocation.reserve(32 + (typeof part === "string" ? part.length * 2 : 0), 1);
4105
+ fragments.push(part);
2688
4106
  }
2689
- io[valueScope]?.reserve(32 + (typeof part === "string" ? part.length * 2 : 0), 0);
2690
- fragments.push(part);
2691
4107
  }
2692
4108
  }
4109
+ return fragments ? concatShellValues(fragments, io[valueScope] ?? allocation) : value;
2693
4110
  }
2694
4111
  finally {
2695
- for (const warning of warnings.splice(0))
2696
- await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
4112
+ try {
4113
+ for (const warning of warnings.splice(0))
4114
+ await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
4115
+ }
4116
+ finally {
4117
+ allocation.close();
4118
+ }
2697
4119
  }
2698
- return fragments ? concatShellValues(fragments, io[valueScope]) : value;
2699
4120
  }
2700
4121
  async redirect(redirects, state, io, inputs, outputs, isolatedInlineInput = false, persistMoves = false, fileShortcut = false, line) {
4122
+ const resourceFs = scopeFileSystem(this.sourceFs, () => this.budget.fileSystemOperation(), this.commandSignal, () => this.budget.fileSystemCleanupOperation());
2701
4123
  this.signal.throwIfAborted();
2702
4124
  if (redirects.length > this.budget.limits.maxRedirects)
2703
4125
  this.budget.fail("maxRedirects");
@@ -2715,6 +4137,17 @@ export class Runtime {
2715
4137
  [2, errorDescriptor?.output === io.stderr ? errorDescriptor : { output: io.stderr }],
2716
4138
  ]);
2717
4139
  const replaced = new Set();
4140
+ const references = new PipeDescriptorFrame(io[invocationScope]);
4141
+ inputs.add(references);
4142
+ const replaceDescriptor = async (number, next) => {
4143
+ const previous = descriptors.get(number)?.pipe;
4144
+ if (previous && references.references.has(previous))
4145
+ await previous.close();
4146
+ if (next)
4147
+ descriptors.set(number, next);
4148
+ else
4149
+ descriptors.delete(number);
4150
+ };
2718
4151
  let errorTarget;
2719
4152
  if (io.stdin === closedSource)
2720
4153
  descriptors.delete(0);
@@ -2727,12 +4160,13 @@ export class Runtime {
2727
4160
  const stdinIsDefault = descriptor?.input ? descriptor.stdinIsDefault : false;
2728
4161
  return {
2729
4162
  [invocationScope]: io[invocationScope],
2730
- [backgroundResources]: io[backgroundResources],
2731
- [backgroundPrune]: io[backgroundPrune],
2732
4163
  ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }),
2733
4164
  ...(io.execution === undefined ? {} : { execution: io.execution }),
2734
4165
  ...(io.diagnosticLine === undefined ? {} : { diagnosticLine: io.diagnosticLine }),
2735
4166
  ...(io.diagnosticOffset === undefined ? {} : { diagnosticOffset: io.diagnosticOffset }),
4167
+ ...(io.assignmentDiagnosticContext === undefined ? {} : { assignmentDiagnosticContext: io.assignmentDiagnosticContext }),
4168
+ ...(io.functionCommandLines === undefined ? {} : { functionCommandLines: io.functionCommandLines }),
4169
+ ...(io.diagnosticCommandLines === undefined ? {} : { diagnosticCommandLines: io.diagnosticCommandLines }),
2736
4170
  ...(io.scriptName === undefined ? {} : { scriptName: io.scriptName }),
2737
4171
  ...(io.substitutionDiagnosticLine === undefined ? {} : { substitutionDiagnosticLine: io.substitutionDiagnosticLine }),
2738
4172
  ...(io.substitutionDiagnosticLines === undefined ? {} : { substitutionDiagnosticLines: io.substitutionDiagnosticLines }),
@@ -2768,9 +4202,17 @@ export class Runtime {
2768
4202
  this.budget.fail("maxExpansionBytes");
2769
4203
  value = concatShellValues([value, "\n"], io[valueScope]);
2770
4204
  }
2771
- const input = new ShellInput(toByteSource(typeof value === "string" ? value : shellValueBytes(value, io[valueScope])), this.budget, this.signal);
2772
- inputs.add(input);
2773
- descriptors.set(redirect.descriptor, { input, stdinIsDefault: false });
4205
+ const prepared = prepareBytesInput(typeof value === "string" ? value : shellValueBytes(value, io[valueScope]), this.budget);
4206
+ const input = new ShellInput(prepared.source, this.budget, this.commandSignal, prepared.options);
4207
+ const lifetime = new DescriptorLifetime(async () => { try {
4208
+ await input.close();
4209
+ }
4210
+ finally {
4211
+ await prepared.close();
4212
+ } });
4213
+ inputs.add({ close: () => lifetime.release() });
4214
+ io[invocationScope].register(() => lifetime.release());
4215
+ await replaceDescriptor(redirect.descriptor, { input, stdinIsDefault: false, lifetime });
2774
4216
  continue;
2775
4217
  }
2776
4218
  const targets = await this.word(redirect.target, state, currentIO());
@@ -2780,7 +4222,7 @@ export class Runtime {
2780
4222
  errorTarget = target;
2781
4223
  if (redirect.operator.endsWith("&")) {
2782
4224
  if (target === "-")
2783
- descriptors.delete(redirect.descriptor);
4225
+ await replaceDescriptor(redirect.descriptor);
2784
4226
  else {
2785
4227
  if (!/^\d+-?$/u.test(target))
2786
4228
  throw new PublicDiagnostic(`${target}: Bad file descriptor`);
@@ -2791,9 +4233,12 @@ export class Runtime {
2791
4233
  const descriptor = descriptors.get(sourceDescriptor);
2792
4234
  if (!descriptor || descriptor.closed || (!move && (redirect.operator === "<&" ? !descriptor.input : !descriptor.output)))
2793
4235
  throw new PublicDiagnostic(`${move ? sourceDescriptor : target}: Bad file descriptor`);
2794
- descriptors.set(redirect.descriptor, { ...descriptor });
4236
+ const pipe = descriptor.pipe && references.acquire(descriptor.pipe);
4237
+ await replaceDescriptor(redirect.descriptor, { ...descriptor, ...(pipe ? { pipe } : {}) });
2795
4238
  if (move && sourceDescriptor !== redirect.descriptor) {
2796
4239
  descriptors.delete(sourceDescriptor);
4240
+ if (descriptor.pipe && (references.references.has(descriptor.pipe) || persistMoves && !replaced.has(sourceDescriptor)))
4241
+ await descriptor.pipe.close();
2797
4242
  if (persistMoves && !replaced.has(sourceDescriptor))
2798
4243
  descriptor.closed = true;
2799
4244
  }
@@ -2801,56 +4246,75 @@ export class Runtime {
2801
4246
  }
2802
4247
  else {
2803
4248
  const path = pathOf(state, target);
2804
- const options = { signal: this.signal };
4249
+ const options = { signal: this.commandSignal };
2805
4250
  if (redirect.operator === "<") {
2806
4251
  await interruptible(this.fs.access(path, 4, options), this.signal);
2807
4252
  const stat = await interruptible(this.fs.stat(path, options), this.signal);
2808
4253
  if (stat.type === "directory" && !fileShortcut)
2809
4254
  throw new PublicDiagnostic(`${target}: Is a directory`);
2810
- const inputScope = (state.backgroundJobs?.execution.scope ?? io[invocationScope]).child();
2811
- let input;
2812
- try {
2813
- const inputSignal = this.commandSignal;
2814
- const inputFs = scopeFileSystem(runtimeFileSystems.get(this.fs) ?? this.fs, () => this.budget.fileSystemOperation(), inputSignal, () => this.budget.fileSystemCleanupOperation());
2815
- const source = stat.type === "directory" ? toByteSource("")
2816
- : await fileInput(inputFs, path, this.budget.limits.maxInputBytes, inputSignal, this.inputProfile, { stat, registerCleanup: cleanup => { inputScope.register(cleanup); } });
2817
- input = new ShellInput(source, this.budget, inputSignal, { stat, ...source });
2818
- inputScopes.set(input, inputScope);
2819
- inputs.add(input);
2820
- }
2821
- catch (error) {
2822
- await inputScope.close();
2823
- throw error;
2824
- }
2825
- descriptors.set(redirect.descriptor, { input, stdinIsDefault: false });
4255
+ const inputOwner = {};
4256
+ let closing;
4257
+ const cleanups = [];
4258
+ const close = () => closing ??= (async () => {
4259
+ const failures = [];
4260
+ try {
4261
+ await inputOwner.input?.close();
4262
+ }
4263
+ catch (error) {
4264
+ failures.push(error);
4265
+ }
4266
+ for (const cleanup of cleanups) {
4267
+ try {
4268
+ await cleanup();
4269
+ }
4270
+ catch (error) {
4271
+ if (!failures.some(failure => Object.is(failure, error)))
4272
+ failures.push(error);
4273
+ }
4274
+ }
4275
+ throwCleanupFailures(failures.filter(error => !(error instanceof PipelineClosed
4276
+ && this.signal.aborted && Object.is(error, this.signal.reason))));
4277
+ })();
4278
+ const lifetime = new DescriptorLifetime(close);
4279
+ inputs.add({ close: () => lifetime.release() });
4280
+ io[invocationScope].register(() => lifetime.release());
4281
+ const prepared = stat.type === "directory" ? prepareBytesInput("", this.budget)
4282
+ : await prepareFileInput({ fs: resourceFs, signal: this.commandSignal, cleanupFailurePrioritySignal: this.budget.signal, registerCleanup: close => {
4283
+ cleanups.push(close);
4284
+ } }, path, this.budget, this.inputProfile, stat);
4285
+ if (!cleanups.includes(prepared.close))
4286
+ cleanups.push(prepared.close);
4287
+ io[invocationScope].assertOpen();
4288
+ const input = new ShellInput(prepared.source, this.budget, this.commandSignal, prepared.options);
4289
+ inputOwner.input = input;
4290
+ await replaceDescriptor(redirect.descriptor, { input, stdinIsDefault: false, lifetime });
2826
4291
  }
2827
4292
  else {
2828
- const outputSignal = this.commandSignal;
2829
- const outputFs = scopeFileSystem(runtimeFileSystems.get(this.fs) ?? this.fs, () => this.budget.fileSystemOperation(), outputSignal, () => this.budget.fileSystemCleanupOperation());
2830
- const options = { signal: outputSignal };
2831
4293
  const append = redirect.operator === ">>";
2832
- const capabilities = await outputFs.capabilitiesFor?.(path, options) ?? outputFs.capabilities;
4294
+ const capabilities = await this.fs.capabilitiesFor?.(path, options) ?? this.fs.capabilities;
4295
+ const canonical = capabilities.open === true;
2833
4296
  const random = capabilities.randomAccessWrite === true;
2834
4297
  const key = resolvePath(state.cwd, path);
2835
4298
  let file;
2836
- await this.fileOperation(key, async () => {
2837
- file = this.outputFiles.get(key) ?? { data: undefined, references: 0 };
2838
- if (!random && file.references)
2839
- throw new FsError("ENOTSUP", { path, message: "Conflicting sequential output descriptors" });
2840
- file.references++;
2841
- this.outputFiles.set(key, file);
2842
- }, outputSignal);
4299
+ if (!canonical)
4300
+ await this.fileOperation(key, async () => {
4301
+ file = this.outputFiles.get(key) ?? { data: undefined, references: 0 };
4302
+ if (!random && capabilities.independentWriteStreams !== true && file.references)
4303
+ throw new FsError("ENOTSUP", { path, message: "Conflicting sequential output descriptors" });
4304
+ file.references++;
4305
+ this.outputFiles.set(key, file);
4306
+ });
2843
4307
  let closed = false;
2844
4308
  let offset = 0;
2845
4309
  const incremental = async () => {
2846
4310
  await this.fileOperation(key, async () => {
2847
4311
  if (append)
2848
- await outputFs.appendFile(path, new Uint8Array(), options);
4312
+ await this.fs.appendFile(path, new Uint8Array(), options);
2849
4313
  else
2850
- await outputFs.writeFile(path, new Uint8Array(), { ...options, flag: "w" });
4314
+ await this.fs.writeFile(path, new Uint8Array(), { ...options, flag: "w" });
2851
4315
  if (!append)
2852
4316
  file.data = new Uint8Array();
2853
- }, outputSignal);
4317
+ });
2854
4318
  return { write: (chunk) => {
2855
4319
  const copy = new Uint8Array(chunk);
2856
4320
  return this.fileOperation(key, async () => {
@@ -2860,18 +4324,18 @@ export class Runtime {
2860
4324
  let atEOF = false;
2861
4325
  if (!append && current && offset === current.length && capabilities.append === true && capabilities.stat !== false) {
2862
4326
  try {
2863
- atEOF = (await interruptible(outputFs.stat(path, options), outputSignal)).size === offset;
4327
+ atEOF = (await interruptible(this.fs.stat(path, options), this.signal)).size === offset;
2864
4328
  }
2865
4329
  catch {
2866
4330
  // Metadata is optional for this optimization; writes need no read access.
2867
- outputSignal.throwIfAborted();
4331
+ this.signal.throwIfAborted();
2868
4332
  }
2869
- outputSignal.throwIfAborted();
4333
+ this.signal.throwIfAborted();
2870
4334
  }
2871
4335
  if (append || atEOF) {
2872
4336
  // Preparing a larger view only touches the unpublished tail of current.
2873
4337
  const bytes = current ? appendOutputBytes(current, copy) : undefined;
2874
- await outputFs.appendFile(path, copy, options);
4338
+ await this.fs.appendFile(path, copy, options);
2875
4339
  file.data = bytes;
2876
4340
  }
2877
4341
  else {
@@ -2879,12 +4343,12 @@ export class Runtime {
2879
4343
  if (current)
2880
4344
  bytes.set(current);
2881
4345
  bytes.set(copy, offset);
2882
- await outputFs.writeFile(path, bytes, options);
4346
+ await this.fs.writeFile(path, bytes, options);
2883
4347
  file.data = bytes;
2884
4348
  }
2885
4349
  if (!append)
2886
4350
  offset += copy.length;
2887
- }, outputSignal);
4351
+ });
2888
4352
  } };
2889
4353
  };
2890
4354
  const resumePathCache = this.budget.pathLookup.suspend();
@@ -2893,14 +4357,27 @@ export class Runtime {
2893
4357
  return;
2894
4358
  closed = true;
2895
4359
  resumePathCache();
4360
+ if (canonical)
4361
+ return;
2896
4362
  if (--file.references === 0 && this.outputFiles.get(key) === file)
2897
4363
  this.outputFiles.delete(key);
2898
4364
  };
2899
4365
  let target;
2900
4366
  let outputScope;
4367
+ const retireOutputCleanups = [];
4368
+ let outputOwner = io[invocationScope];
4369
+ while (outputOwner.parent)
4370
+ outputOwner = outputOwner.parent;
2901
4371
  try {
2902
- outputScope = (state.backgroundJobs?.execution.scope ?? io[invocationScope]).child();
2903
- target = await openFileOutput({ fs: outputFs, signal: outputSignal, registerCleanup: cleanup => { outputScope.register(cleanup); } }, path, append ? "a" : "w", random ? incremental : undefined);
4372
+ outputScope = new InvocationScope(this.commandSignal, io[invocationScope].failures);
4373
+ const context = { fs: resourceFs, signal: this.commandSignal, cleanupFailurePrioritySignal: this.budget.signal, registerCleanup: (cleanup) => {
4374
+ const retire = (canonical ? outputOwner : outputScope).register(cleanup);
4375
+ if (canonical)
4376
+ retireOutputCleanups.push(retire);
4377
+ } };
4378
+ if (canonical)
4379
+ bindFileOutputBudget(context, sink => this.budget.sink(sink, this.commandSignal), (chunk, write) => this.budget.writeCounted(chunk, write, this.commandSignal));
4380
+ target = await openFileOutput(context, path, canonical ? { flag: append ? "a" : "w", descriptor: true } : append ? "a" : "w", !canonical && random ? incremental : undefined);
2904
4381
  }
2905
4382
  catch (error) {
2906
4383
  try {
@@ -2911,19 +4388,21 @@ export class Runtime {
2911
4388
  }
2912
4389
  throw error;
2913
4390
  }
2914
- outputs.add(async (completion) => {
4391
+ const finalize = async (completion) => {
2915
4392
  try {
2916
- if (outputSignal.aborted)
2917
- await target.abort(outputSignal.reason);
4393
+ if (this.commandSignal.aborted)
4394
+ await target.abort(this.commandSignal.reason);
2918
4395
  else if ("reason" in completion)
2919
4396
  await target.abort(completion.reason);
2920
4397
  else {
2921
4398
  try {
2922
4399
  await target.finish();
4400
+ for (const retire of retireOutputCleanups)
4401
+ retire();
2923
4402
  }
2924
4403
  catch (error) {
2925
- outputSignal.throwIfAborted();
2926
- if (completion.status === 0)
4404
+ this.signal.throwIfAborted();
4405
+ if (completion.status === 0 || target.descriptor && !target.signal.aborted)
2927
4406
  throw error;
2928
4407
  }
2929
4408
  }
@@ -2936,13 +4415,20 @@ export class Runtime {
2936
4415
  release();
2937
4416
  }
2938
4417
  }
2939
- });
2940
- const output = this.budget.sink(target.sink, outputSignal);
4418
+ };
4419
+ let completion;
4420
+ const lifetime = new DescriptorLifetime(() => finalize(completion ?? { status: 0 }));
4421
+ const ownerFinalize = value => { completion ??= value; return lifetime.release(); };
4422
+ outputs.add(Object.assign(ownerFinalize, target.descriptor ? { descriptor: target.descriptor } : {}));
4423
+ const output = canonical ? target.sink : this.budget.sink(target.sink, this.commandSignal);
4424
+ if (canonical)
4425
+ budgetedSinks.set(output, { budget: this.budget, write: output.write });
2941
4426
  budgetedSinks.get(output).file = Object.freeze({ path });
2942
- descriptors.set(redirect.descriptor, { output });
4427
+ const binding = { output, lifetime, ...(target.descriptor ? { file: target.descriptor } : {}) };
4428
+ await replaceDescriptor(redirect.descriptor, binding);
2943
4429
  if (redirect.operator === "&>") {
2944
4430
  replaced.add(2);
2945
- descriptors.set(2, { output });
4431
+ await replaceDescriptor(2, { ...binding });
2946
4432
  }
2947
4433
  }
2948
4434
  }
@@ -2952,15 +4438,14 @@ export class Runtime {
2952
4438
  const diagnostic = errorTarget === undefined ? undefined : filesystemDiagnostic(error, errorTarget);
2953
4439
  throw new ExecutionFailure(error, currentIO(), diagnostic);
2954
4440
  }
2955
- io[backgroundPrune]?.(descriptors);
2956
4441
  return currentIO();
2957
4442
  }
2958
- async fileOperation(path, operation, signal = this.signal) {
4443
+ async fileOperation(path, operation) {
2959
4444
  const previous = this.fileWrites.get(path) ?? Promise.resolve();
2960
- const pending = previous.catch(() => undefined).then(() => { signal.throwIfAborted(); return operation(); });
4445
+ const pending = previous.catch(() => undefined).then(() => { this.signal.throwIfAborted(); return operation(); });
2961
4446
  this.fileWrites.set(path, pending);
2962
4447
  try {
2963
- await interruptible(pending, signal);
4448
+ await interruptible(pending, this.signal);
2964
4449
  }
2965
4450
  finally {
2966
4451
  if (this.fileWrites.get(path) === pending)
@@ -2976,7 +4461,7 @@ export class Runtime {
2976
4461
  return undefined;
2977
4462
  return { name: match[1], append: match[2] === "+", value: { offset: word.offset, parts: [{ ...first, value: first.value.slice(match[0].length) }, ...word.parts.slice(1)] } };
2978
4463
  }
2979
- async simple(command, state, originalIO, inputs, outputs, createSnapshotScope, fileShortcut = false) {
4464
+ async simple(command, state, originalIO, inputs, outputs, createSnapshotScope, fileShortcut = false, terminal) {
2980
4465
  state.substitutionStatus = 0;
2981
4466
  const assignments = [];
2982
4467
  let wordIndex = 0;
@@ -2988,17 +4473,21 @@ export class Runtime {
2988
4473
  }
2989
4474
  const commandWords = command.words.slice(wordIndex);
2990
4475
  let declarationIndex = 0;
2991
- while (commandWords[declarationIndex]?.plain === "command") {
4476
+ while ((commandWords[declarationIndex]?.plain === "command" || commandWords[declarationIndex]?.plain === "builtin") && !state.extensions?.builtins.has(commandWords[declarationIndex].plain)) {
2992
4477
  declarationIndex++;
2993
4478
  if (commandWords[declarationIndex]?.plain === "--")
2994
4479
  declarationIndex++;
2995
4480
  }
2996
- const wordValues = await this.valueWords(commandWords, state, originalIO, ["export", "local", "readonly"].includes(commandWords[declarationIndex]?.plain ?? ""));
4481
+ const declarationName = commandWords[declarationIndex]?.plain ?? "";
4482
+ const declarationBuiltin = state.extensions?.builtins.get(declarationName);
4483
+ const declaration = declarationBuiltin ? declarationBuiltin.expansion === "declaration" : ["export", "local", "readonly"].includes(declarationName);
4484
+ const indexedDeclaration = declaration && state.extensions?.syntax.indexedDeclarations?.some(name => name === declarationName) === true;
4485
+ const wordValues = await this.valueWords(commandWords, state, originalIO, declaration, indexedDeclaration);
2997
4486
  const words = wordValues.map(shellValueText);
2998
- const special = state.profile === "sh" && specialBuiltinNames.has(words[0] ?? "");
4487
+ const special = state.profile === "sh" && (specialBuiltinNames.has(words[0] ?? "") || !!state.extensions?.builtins.get(words[0] ?? "")?.special);
2999
4488
  const inlineInput = command.redirects.some((redirect) => redirect.document || redirect.operator === "<<<");
3000
4489
  const functionCommand = words.length > 0 && state.functions.has(words[0]);
3001
- const isolatedInlineInput = inlineInput && words.length > 0 && !shellBuiltinNames.has(words[0]) && !functionCommand;
4490
+ const isolatedInlineInput = inlineInput && words.length > 0 && !shellBuiltinNames.has(words[0]) && !state.extensions?.builtins.has(words[0]) && !functionCommand;
3002
4491
  const snapshotScope = isolatedInlineInput ? createSnapshotScope() : undefined;
3003
4492
  let io = snapshotScope ? { ...originalIO, [invocationScope]: snapshotScope } : originalIO;
3004
4493
  const previous = new Map();
@@ -3015,7 +4504,7 @@ export class Runtime {
3015
4504
  }
3016
4505
  await this.arrayZero(state, assignment.name, async () => {
3017
4506
  const fields = await this.valueWord(assignment.value, state, io, false);
3018
- return fields.every(value => typeof value === "string") ? this.arrayJoin(requireArrays(state).owner, fields, "") : concatShellValues(fields, io[valueScope]);
4507
+ return this.arrayJoin(requireArrays(state).owner, fields, "");
3019
4508
  }, assignment.append);
3020
4509
  continue;
3021
4510
  }
@@ -3041,11 +4530,8 @@ export class Runtime {
3041
4530
  };
3042
4531
  let overlayOpen = false;
3043
4532
  try {
3044
- if (snapshotScope) {
3045
- const jobs = state.backgroundJobs;
4533
+ if (snapshotScope)
3046
4534
  state = await cloneState(state, this.signal, snapshotScope);
3047
- state.backgroundJobs = jobs;
3048
- }
3049
4535
  if (words.length) {
3050
4536
  stateMonitor(state)?.openOverlay(previous);
3051
4537
  overlayOpen = true;
@@ -3129,7 +4615,11 @@ export class Runtime {
3129
4615
  }
3130
4616
  }
3131
4617
  else
3132
- io = await this.redirect(command.redirects, state, io, inputs, outputs, isolatedInlineInput, !words.length || shellBuiltinNames.has(words[0]) || functionCommand, fileShortcut, command.line ?? 1);
4618
+ io = await this.redirect(command.redirects, state, io, inputs, outputs, isolatedInlineInput, !words.length || shellBuiltinNames.has(words[0]) || !!state.extensions?.builtins.has(words[0]) || functionCommand, fileShortcut, command.line ?? 1);
4619
+ if (terminal) {
4620
+ terminal.io = io;
4621
+ await terminal.frame.reconcile(io.descriptors);
4622
+ }
3133
4623
  if (!inlineInput)
3134
4624
  await assign();
3135
4625
  if (fileShortcut) {
@@ -3139,11 +4629,14 @@ export class Runtime {
3139
4629
  await pipeBytes(input, io.stdout, this.signal);
3140
4630
  return 0;
3141
4631
  }
3142
- return words.length ? await this.dispatch(words[0], words.slice(1), state, io, previous, false, wordValues.slice(1)) : state.substitutionStatus;
4632
+ return words.length ? await this.dispatch(wordValues[0], words.slice(1), state, io, previous, false, wordValues.slice(1), previous) : state.substitutionStatus;
3143
4633
  }
3144
4634
  catch (error) {
3145
- if (error instanceof Flow)
4635
+ if (error instanceof Flow) {
4636
+ if ((error.kind === "break" || error.kind === "continue" || error.kind === "return") && io.assignmentDiagnosticContext)
4637
+ io.assignmentDiagnosticContext.name = undefined;
3146
4638
  throw error;
4639
+ }
3147
4640
  this.signal.throwIfAborted();
3148
4641
  const original = error instanceof ExecutionFailure ? error.original : error;
3149
4642
  if (special && !(original instanceof ShellLimitError) && !(original instanceof ExpansionFailure) && !(original instanceof Flow) && !(original instanceof ShellSyntaxError)) {
@@ -3169,23 +4662,26 @@ export class Runtime {
3169
4662
  saved.heldValue?.release();
3170
4663
  }
3171
4664
  }
3172
- async dispatch(name, args, state, io, assignments, bypassFunctions = false, values = args) {
4665
+ async dispatch(name, args, state, io, assignments, bypassFunctions = false, values = args, temporaryEnvironment) {
3173
4666
  const scope = io[invocationScope].child();
3174
4667
  const runtime = new Runtime(this.fs, this.commands, this.middleware, this.budget, AbortSignal.any([this.signal, scope.signal]), this.fileWrites, this.outputFiles, this.commandSignal, this.cancellation, this.cancellationState, this.cancellationOwner, this.cancellationDepth, this.cancellationMaxDepth, this.outcomeFrame, this.inputProfile);
3175
4668
  try {
3176
- return await runtime.dispatchScoped(name, values, state, { ...io, [invocationScope]: scope }, assignments, bypassFunctions);
4669
+ return await runtime.dispatchScoped(name, values, state, { ...io, [invocationScope]: scope }, assignments, bypassFunctions, temporaryEnvironment);
3177
4670
  }
3178
4671
  finally {
3179
4672
  await scope.close();
3180
4673
  }
3181
4674
  }
3182
- async dispatchScoped(name, values, state, io, assignments, bypassFunctions) {
4675
+ async dispatchScoped(nameValue, values, state, io, assignments, bypassFunctions, temporaryEnvironment) {
3183
4676
  const { [invocationScope]: scope, ...publicIO } = io;
3184
4677
  Reflect.deleteProperty(publicIO, valueScope);
3185
- Reflect.deleteProperty(publicIO, backgroundResources);
3186
- Reflect.deleteProperty(publicIO, backgroundPrune);
3187
4678
  const allocation = this.budget.values.scope();
3188
4679
  scope.register(() => allocation.close());
4680
+ if (typeof nameValue !== "string")
4681
+ allocation.hold(nameValue);
4682
+ const name = shellValueText(nameValue);
4683
+ let currentName = nameValue;
4684
+ const readName = () => shellValueText(currentName);
3189
4685
  const argumentValues = this.admitArguments(values, allocation);
3190
4686
  let builtinFailure;
3191
4687
  const env = Object.create(null);
@@ -3217,8 +4713,13 @@ export class Runtime {
3217
4713
  return invocation;
3218
4714
  },
3219
4715
  };
4716
+ if (typeof nameValue !== "string")
4717
+ Object.defineProperty(context, "command", {
4718
+ configurable: true, enumerable: true, get: readName,
4719
+ set(replacement) { currentName = replacement; },
4720
+ });
3220
4721
  bindCommandIO(context);
3221
- bindFileOutputBudget(context, sink => this.budget.sink(sink, this.signal));
4722
+ bindFileOutputBudget(context, sink => this.budget.sink(sink, this.signal), (chunk, write) => this.budget.writeCounted(chunk, write, this.signal));
3222
4723
  if (argumentValues.values.every(value => typeof value === "string"))
3223
4724
  Reflect.deleteProperty(context, "argumentValues");
3224
4725
  const middleware = this.middleware.map((handler) => (context, next) => {
@@ -3233,10 +4734,10 @@ export class Runtime {
3233
4734
  });
3234
4735
  const execute = composeMiddleware(middleware, (forwarded) => scope.run(async () => {
3235
4736
  scope.assertOpen();
4737
+ const commandName = Object.getOwnPropertyDescriptor(forwarded, "command")?.get === readName ? currentName : forwarded.command;
3236
4738
  const forwardedValues = getCommandArguments(forwarded);
3237
4739
  const admitted = forwardedValues === argumentValues ? argumentValues : this.admitArguments(forwardedValues.values, allocation);
3238
4740
  const context = { ...forwarded, args: admitted.args, argumentValues: admitted, [invocationScope]: scope,
3239
- [backgroundResources]: io[backgroundResources], [backgroundPrune]: io[backgroundPrune],
3240
4741
  ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }) };
3241
4742
  const previous = new Map();
3242
4743
  const cwd = state.cwd;
@@ -3376,15 +4877,71 @@ export class Runtime {
3376
4877
  const callerLoopDepth = state.loopDepth;
3377
4878
  if (mapfileCallbackStates.has(state))
3378
4879
  state.loopDepth = 0;
4880
+ let restoredLocals = false;
4881
+ const restoreLocals = async () => {
4882
+ if (restoredLocals)
4883
+ return;
4884
+ restoredLocals = true;
4885
+ for (const [name, previous] of locals)
4886
+ await scope.cleanup(async () => {
4887
+ const typed = typedSavedVariables.has(previous);
4888
+ await restoreVariable(state, name, previous);
4889
+ if (!typed && !previous.readOnly)
4890
+ state.readonlyVariables?.delete(name);
4891
+ });
4892
+ };
4893
+ let primaryFailure = false;
4894
+ let outcome;
4895
+ let checkpointFailure;
3379
4896
  try {
3380
- return { exitCode: await this.command(body, state, { ...io, ...context, scriptName: body.sourceName ?? io.scriptName ?? "shell" }) };
4897
+ try {
4898
+ await this.extensionEvent("function-enter", state, io, state.status);
4899
+ if (await this.extensionEvent("command", state, { ...io, ...context }, state.status, context.command))
4900
+ outcome = { kind: "return", value: { exitCode: state.status } };
4901
+ else {
4902
+ const diagnostic = functionDiagnostics.get(body);
4903
+ const status = await this.command(body, state, {
4904
+ ...io, ...context, scriptName: body.sourceName ?? io.scriptName ?? "shell",
4905
+ diagnosticOffset: diagnostic?.offset ?? 0,
4906
+ assignmentDiagnosticContext: { name: context.command },
4907
+ functionCommandLines: diagnostic?.lines, diagnosticCommandLines: diagnostic?.lines,
4908
+ });
4909
+ outcome = { kind: "return", value: { exitCode: await this.finishReturn("function-return", state, { ...io, ...context }, status) } };
4910
+ }
4911
+ }
4912
+ catch (error) {
4913
+ if (error instanceof NounsetDiagnosticFailure)
4914
+ throw error;
4915
+ if (error instanceof Flow && error.kind === "return") {
4916
+ outcome = { kind: "return", value: { exitCode: await this.finishReturn("function-return", state, { ...io, ...context }, error.status, error.previousStatus) } };
4917
+ }
4918
+ else {
4919
+ if (error instanceof Flow && error.kind === "exit" && state.extensions) {
4920
+ if (error.previousStatus === undefined)
4921
+ await restoreLocals();
4922
+ throw new Flow("exit", await this.beginShellExit(state, { ...io, ...context }, error.status));
4923
+ }
4924
+ throw error;
4925
+ }
4926
+ }
3381
4927
  }
3382
4928
  catch (error) {
3383
- if (error instanceof Flow && error.kind === "return")
3384
- return { exitCode: error.status };
3385
- throw error;
4929
+ primaryFailure = !(error instanceof Flow) || error instanceof NounsetDiagnosticFailure;
4930
+ outcome = { kind: "throw", reason: error };
3386
4931
  }
3387
4932
  finally {
4933
+ await scope.cleanup(async () => {
4934
+ try {
4935
+ if (!this.signal.aborted)
4936
+ await this.extensionEvent("function-leave", state, io, state.status);
4937
+ }
4938
+ catch (error) {
4939
+ if (error instanceof ExtensionCheckpointFailure && !primaryFailure)
4940
+ checkpointFailure = error;
4941
+ else
4942
+ throw error;
4943
+ }
4944
+ });
3388
4945
  const restoreControls = () => {
3389
4946
  stateMonitor(state).positionals.restore(savedPositionals, () => { state.positional = positional; });
3390
4947
  savedPositionals.close();
@@ -3399,14 +4956,8 @@ export class Runtime {
3399
4956
  functionRestoration.apply(restoreControls, false);
3400
4957
  else
3401
4958
  restoreControls();
3402
- });
3403
- for (const [name, previous] of locals)
3404
- await scope.cleanup(async () => {
3405
- const typed = typedSavedVariables.has(previous);
3406
- await restoreVariable(state, name, previous);
3407
- if (!typed && !previous.readOnly)
3408
- state.readonlyVariables?.delete(name);
3409
- });
4959
+ });
4960
+ await restoreLocals();
3410
4961
  if (locals.has("OPTIND"))
3411
4962
  await scope.cleanup(() => {
3412
4963
  const restoreGetopts = () => {
@@ -3421,13 +4972,27 @@ export class Runtime {
3421
4972
  await scope.cleanup(() => getoptsRestoration?.close());
3422
4973
  await scope.cleanup(() => functionRestoration?.close());
3423
4974
  }
4975
+ if (checkpointFailure) {
4976
+ this.signal.throwIfAborted();
4977
+ throw checkpointFailure;
4978
+ }
4979
+ if (outcome.kind === "throw")
4980
+ throw outcome.reason;
4981
+ return outcome.value;
3424
4982
  }
3425
4983
  if (selected?.kind === "builtin") {
3426
- if (context.command === "command" || context.command === "builtin" || context.command === "type")
3427
- return { exitCode: await this.discoveryBuiltin(context, state, io, assignments) };
3428
- const special = state.profile === "sh" && !bypassFunctions && specialBuiltinNames.has(context.command);
4984
+ const extensionBuiltin = state.extensions?.builtins.get(context.command);
4985
+ const special = state.profile === "sh" && !bypassFunctions && (specialBuiltinNames.has(context.command) || !!extensionBuiltin?.special);
3429
4986
  if (special)
3430
4987
  assignments.clear();
4988
+ if (extensionBuiltin) {
4989
+ const status = validateExitCode(await extensionBuiltin.execute(this.extensionContext(state, { ...io, ...context }, context.command, context.args, context.argumentValues.values)));
4990
+ if (special && status !== 0)
4991
+ throw new Flow("exit", status);
4992
+ return { exitCode: status };
4993
+ }
4994
+ if (context.command === "command" || context.command === "builtin" || context.command === "type")
4995
+ return { exitCode: await this.discoveryBuiltin(context, state, io, assignments) };
3431
4996
  if (context.command === "." || context.command === "source")
3432
4997
  return { exitCode: await this.sourceBuiltin(context, state, { ...io, ...context }, special) };
3433
4998
  if (context.command === "eval")
@@ -3452,7 +5017,12 @@ export class Runtime {
3452
5017
  const target = await this.searchPath(context.command, state);
3453
5018
  if (target !== undefined)
3454
5019
  return { exitCode: await this.scriptFile(context, state, io, target, context.args, true) };
3455
- await this.diagnostic({ ...io, ...context }, `${context.command}: command not found`);
5020
+ const localeValue = (key) => temporaryEnvironment?.has(key) && !previous.has(key)
5021
+ ? temporaryEnvironment.get(key).value ?? "" : state.variables[key] ?? "";
5022
+ const displayed = diagnosticCommandName(commandName, byteLocale({
5023
+ LC_ALL: localeValue("LC_ALL"), LC_CTYPE: localeValue("LC_CTYPE"), LANG: localeValue("LANG"),
5024
+ }), allocation);
5025
+ await this.diagnostic({ ...io, ...context }, concatShellValues([displayed, ": command not found"], allocation));
3456
5026
  return { exitCode: 127 };
3457
5027
  }
3458
5028
  this.budget.pathLookup.suspendUntilClosed(scope);
@@ -3508,13 +5078,13 @@ export class Runtime {
3508
5078
  const matches = [];
3509
5079
  if (!bypassFunctions && state.functions.has(name))
3510
5080
  matches.push({ kind: "function", name });
3511
- if (implementedBuiltins.has(name))
5081
+ if (implementedBuiltins.has(name) || state.extensions?.builtins.has(name))
3512
5082
  matches.push({ kind: "builtin", name });
3513
5083
  else if (this.commands.has(name))
3514
5084
  matches.push({ kind: "command", name });
3515
5085
  else if (name === "bash" || name === "sh")
3516
5086
  matches.push({ kind: "interpreter", name });
3517
- if (state.profile === "sh" && specialBuiltinNames.has(name))
5087
+ if (state.profile === "sh" && (specialBuiltinNames.has(name) || state.extensions?.builtins.get(name)?.special))
3518
5088
  matches.sort((left, right) => Number(right.kind === "builtin") - Number(left.kind === "builtin"));
3519
5089
  return matches;
3520
5090
  }
@@ -3563,11 +5133,13 @@ export class Runtime {
3563
5133
  }
3564
5134
  }
3565
5135
  if (!discover) {
5136
+ const targetIndex = context.args.length - args.length;
3566
5137
  const target = args.shift();
3567
5138
  if (target === undefined)
3568
5139
  return 0;
3569
- if (builtin && !shellBuiltinNames.has(target)) {
3570
- await this.diagnostic({ ...io, ...context }, `builtin: ${target}: not a shell builtin`);
5140
+ const targetValue = getCommandArguments(context).values[targetIndex];
5141
+ if (builtin && !shellBuiltinNames.has(target) && !state.extensions?.builtins.has(target)) {
5142
+ await this.diagnostic({ ...io, ...context }, concatShellValues(["builtin: ", targetValue, ": not a shell builtin"], io[valueScope]));
3571
5143
  return 1;
3572
5144
  }
3573
5145
  this.budget.tick();
@@ -3582,7 +5154,7 @@ export class Runtime {
3582
5154
  throw error;
3583
5155
  }
3584
5156
  try {
3585
- return await this.dispatch(target, args, state, { ...io, ...context }, assignments, true, getCommandArguments(context).values.slice(context.args.length - args.length));
5157
+ return await this.dispatch(targetValue, args, state, { ...io, ...context }, assignments, true, getCommandArguments(context).values.slice(context.args.length - args.length));
3586
5158
  }
3587
5159
  finally {
3588
5160
  const restore = () => { state.depth--; };
@@ -3676,7 +5248,7 @@ export class Runtime {
3676
5248
  throw denied;
3677
5249
  return matches;
3678
5250
  }
3679
- processState(context, state, arg0, args, io) {
5251
+ processState(context, state, io, arg0, args) {
3680
5252
  if (state.depth >= this.budget.limits.maxSubstitutionDepth)
3681
5253
  this.budget.fail("maxSubstitutionDepth");
3682
5254
  const variables = Object.assign(Object.create(null), context.env, { PWD: state.cwd });
@@ -3684,11 +5256,11 @@ export class Runtime {
3684
5256
  variables.OPTIND = "1";
3685
5257
  variables.OPTERR = "1";
3686
5258
  const child = trackState({
5259
+ extensions: forkExtensions(state.extensions, "process"),
3687
5260
  cwd: state.cwd, variables, exported, functions: new Map(), getopts: { cursor: createGetoptsState(), integer: true },
3688
5261
  directoryStack: { entries: [], bytes: 0 },
3689
5262
  dotglob: false,
3690
5263
  globstar: false,
3691
- backgroundJobs: new BackgroundJobs(state.backgroundJobs.execution),
3692
5264
  positional: [], arg0: shellValueText(arg0), profile: context.command === "sh" ? "sh" : "bash", status: 0, substitutionStatus: 0, depth: state.depth + 1,
3693
5265
  loopDepth: 0, functionDepth: 0, locals: [], pipefail: false, isolated: true,
3694
5266
  errexit: false,
@@ -3730,28 +5302,37 @@ export class Runtime {
3730
5302
  await writeDiagnostic(context.stderr, `${context.command}: -c: option requires an argument\n`);
3731
5303
  return 2;
3732
5304
  }
3733
- const zeroIndex = context.args.length - args.length;
3734
- const arg0 = commandString ? args.shift() ?? context.command : context.command;
3735
- const zeroValue = commandString && zeroIndex < context.args.length ? getCommandArguments(context).values[zeroIndex] : arg0;
3736
- const child = this.processState(context, state, zeroValue, args, io);
5305
+ const arg0 = commandString && args.length ? getCommandArguments(context).values[context.args.length - args.length] : context.command;
5306
+ if (commandString)
5307
+ args.shift();
5308
+ const child = this.processState(context, state, io, arg0, args);
3737
5309
  child.errexit = errexit;
3738
5310
  child.braceexpand = braceexpand;
3739
- const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: arg0 });
3740
- if (source !== undefined) {
3741
- this.budget.source(Buffer.byteLength(source));
3742
- return this.runCommandString(source, child, childIO);
5311
+ const references = new PipeDescriptorFrame(io[invocationScope]);
5312
+ const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, assignmentDiagnosticContext: undefined, scriptName: shellValueText(arg0) }, references);
5313
+ try {
5314
+ if (source !== undefined) {
5315
+ this.budget.source(Buffer.byteLength(source));
5316
+ return await this.finishShell(child, childIO, await this.runCommandString(source, child, childIO));
5317
+ }
5318
+ const input = new ShellInput(context.stdin, this.budget, this.signal);
5319
+ const descriptors = new Map(childIO.descriptors);
5320
+ descriptors.set(0, { ...descriptors.get(0), input });
5321
+ const inputIO = { ...childIO, stdin: input, descriptors };
5322
+ return await this.finishShell(child, inputIO, await this.runStandardInput(input, child, inputIO));
5323
+ }
5324
+ finally {
5325
+ await references.close();
3743
5326
  }
3744
- const input = new ShellInput(context.stdin, this.budget, this.signal);
3745
- return this.runStandardInput(input, child, { ...childIO, stdin: input });
3746
5327
  }
3747
- async syntaxFailure(error, source, io, commandString) {
5328
+ async syntaxFailure(error, source, io, commandString, includeContext = true) {
3748
5329
  const offset = io.diagnosticOffset ?? 0;
3749
5330
  const line = source.slice(0, error.offset).split("\n").length;
3750
5331
  const prefix = `${io.scriptName ?? "shell"}:${commandString ? " -c:" : ""}`;
3751
5332
  if (error.unclosedQuote)
3752
5333
  await writeDiagnostic(io.stderr, `${prefix} line ${offset + error.unclosedQuote.line}: unexpected EOF while looking for matching \`${error.unclosedQuote.quote}'\n`);
3753
5334
  else if (error.offset >= source.length && !/Unterminated|nesting|Unsupported/u.test(error.reason)) {
3754
- const context = error.incompleteCommand ? ` from \`${error.incompleteCommand.name}' command on line ${offset + error.incompleteCommand.line}` : "";
5335
+ const context = includeContext && error.incompleteCommand ? ` from \`${error.incompleteCommand.name}' command on line ${offset + error.incompleteCommand.line}` : "";
3755
5336
  await writeDiagnostic(io.stderr, `${prefix} line ${offset + source.split("\n").length + Number(!source.endsWith("\n"))}: syntax error: unexpected end of file${context}\n`);
3756
5337
  }
3757
5338
  else {
@@ -3767,7 +5348,7 @@ export class Runtime {
3767
5348
  try {
3768
5349
  do {
3769
5350
  this.signal.throwIfAborted();
3770
- const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing, lineIndex);
5351
+ const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing, lineIndex, undefined, false, state.extensions?.syntax);
3771
5352
  for (const warning of unit.script.warnings ?? [])
3772
5353
  await writeDiagnostic(io.stderr, `${io.scriptName}: warning: ${warning}\n`);
3773
5354
  if (unit.script.lists.length) {
@@ -3814,7 +5395,7 @@ export class Runtime {
3814
5395
  }
3815
5396
  const unitIO = { ...io, diagnosticOffset: offset };
3816
5397
  try {
3817
- const unit = eof ? parseShellUnit(source, 0, byteLocale(state.variables), this.budget.parsing, lineIndex) : parseShellInputUnit(source, byteLocale(state.variables), this.budget.parsing, lineIndex);
5398
+ const unit = eof ? parseShellUnit(source, 0, byteLocale(state.variables), this.budget.parsing, lineIndex, undefined, false, state.extensions?.syntax) : parseShellInputUnit(source, byteLocale(state.variables), this.budget.parsing, lineIndex, state.extensions?.syntax);
3818
5399
  if (unit) {
3819
5400
  for (const warning of unit.script.warnings ?? [])
3820
5401
  await writeDiagnostic(io.stderr, `${io.scriptName}: warning: ${warning}\n`);
@@ -3895,10 +5476,8 @@ export class Runtime {
3895
5476
  };
3896
5477
  Reflect.deleteProperty(context, invocationScope);
3897
5478
  Reflect.deleteProperty(context, valueScope);
3898
- Reflect.deleteProperty(context, backgroundResources);
3899
- Reflect.deleteProperty(context, backgroundPrune);
3900
5479
  bindCommandIO(context);
3901
- bindFileOutputBudget(context, sink => this.budget.sink(sink, runtime.signal));
5480
+ bindFileOutputBudget(context, sink => this.budget.sink(sink, runtime.signal), (chunk, write) => this.budget.writeCounted(chunk, write, runtime.signal));
3902
5481
  if (argumentValues.values.every(value => typeof value === "string"))
3903
5482
  Reflect.deleteProperty(context, "argumentValues");
3904
5483
  const child = await runtime.shebangState(context, state);
@@ -4046,6 +5625,8 @@ export class Runtime {
4046
5625
  const path = pathOf(state, target);
4047
5626
  let source;
4048
5627
  let environmentInterpreter = null;
5628
+ let interpreterCommand;
5629
+ let sourceBytes;
4049
5630
  let interpreterProfile;
4050
5631
  try {
4051
5632
  if (loadedSource?.path === path)
@@ -4067,18 +5648,33 @@ export class Runtime {
4067
5648
  this.budget.fail("maxSourceBytes");
4068
5649
  const bytes = await interruptible(this.fs.readFile(path, { ...options, maxBytes }), this.signal);
4069
5650
  this.budget.source(bytes.byteLength);
4070
- source = this.sourceText(bytes, target);
5651
+ sourceBytes = bytes;
5652
+ const newline = bytes.indexOf(10);
5653
+ source = direct && bytes[0] === 35 && bytes[1] === 33
5654
+ ? this.sourceText(bytes.subarray(0, newline < 0 ? bytes.length : newline), target)
5655
+ : this.sourceText(bytes, target);
4071
5656
  }
4072
5657
  if (direct && source.startsWith("#!")) {
4073
5658
  const interpreter = source.split("\n", 1)[0].slice(2).replace(/^[ \t]+|[ \t]+$/gu, "");
4074
5659
  environmentInterpreter = /^\/usr\/bin\/env(?:[ \t]+([^\n]*))?$/u.exec(interpreter);
4075
5660
  if (!environmentInterpreter) {
4076
5661
  const shell = /^\/(?:usr\/)?bin\/(bash|sh)(?:[ \t]+([-+]e+))?$/u.exec(interpreter);
4077
- if (!shell)
4078
- throw new CommandFailure(`${target}: unsupported interpreter: ${interpreter}`, 126);
4079
- interpreterProfile = shell[1] === "sh" ? "sh" : "bash";
4080
- if (shell[2])
4081
- errexit = shell[2].startsWith("-");
5662
+ if (!shell) {
5663
+ const split = Array.from(interpreter).findIndex(character => character === " " || character === "\t");
5664
+ const executable = split < 0 ? interpreter : interpreter.slice(0, split);
5665
+ const name = executable.slice(executable.lastIndexOf("/") + 1);
5666
+ const directory = executable.slice(0, executable.lastIndexOf("/"));
5667
+ if ((directory !== "/bin" && directory !== "/usr/bin") || name === "sh" || name === "bash" || !this.commands.has(name)) {
5668
+ throw new CommandFailure(`${target}: unsupported interpreter: ${interpreter}`, 126);
5669
+ }
5670
+ const argument = split < 0 ? undefined : interpreter.slice(split).trimStart();
5671
+ interpreterCommand = { name, ...(argument ? { argument } : {}) };
5672
+ }
5673
+ else {
5674
+ interpreterProfile = shell[1] === "sh" ? "sh" : "bash";
5675
+ if (shell[2])
5676
+ errexit = shell[2].startsWith("-");
5677
+ }
4082
5678
  }
4083
5679
  }
4084
5680
  }
@@ -4090,15 +5686,25 @@ export class Runtime {
4090
5686
  this.budget.fail("maxSourceBytes");
4091
5687
  throw new CommandFailure(filesystemDiagnostic(error, target, this.budget.onInternalError) ?? `${target}: ${message(error, this.budget.onInternalError)}`, errorCode(error) === "ENOENT" ? 127 : 126);
4092
5688
  }
5689
+ const decodeSource = () => sourceBytes ? this.sourceText(sourceBytes, target) : source;
5690
+ const scriptSource = { path, get source() { return decodeSource(); } };
4093
5691
  if (direct && environmentInterpreter)
4094
- return this.envShebang(context, state, io, environmentInterpreter[1], target, args, { path, source });
5692
+ return this.envShebang(context, state, io, environmentInterpreter[1], target, args, scriptSource);
5693
+ if (direct && interpreterCommand) {
5694
+ const incoming = getCommandArguments({ args, ...(context.argumentValues ? { argumentValues: context.argumentValues } : {}) });
5695
+ const allocation = this.budget.values.scope();
5696
+ io[invocationScope].register(() => allocation.close());
5697
+ const argumentValues = this.admitArguments([...(interpreterCommand.argument ? [interpreterCommand.argument] : []), target, ...incoming.values], allocation);
5698
+ return (await this.shebangTarget(context, state, io, interpreterCommand.name, argumentValues.args, { argumentValues }, target, scriptSource)).exitCode;
5699
+ }
5700
+ source = scriptSource.source;
4095
5701
  const units = [];
4096
5702
  const lineIndex = new SourceLineIndex(source, this.budget.parsing);
4097
5703
  try {
4098
5704
  let position = 0;
4099
5705
  do {
4100
5706
  this.signal.throwIfAborted();
4101
- const unit = parseShellUnit(source, position, byteLocale(context.env), this.budget.parsing, lineIndex);
5707
+ const unit = parseShellUnit(source, position, byteLocale(context.env), this.budget.parsing, lineIndex, undefined, false, state.extensions?.syntax);
4102
5708
  units.push(unit.script);
4103
5709
  position = unit.next;
4104
5710
  } while (position < source.length);
@@ -4110,26 +5716,32 @@ export class Runtime {
4110
5716
  await writeDiagnostic(context.stderr, `${target}: line ${line}: syntax error: ${error.reason}\n`);
4111
5717
  return error.exitCode;
4112
5718
  }
4113
- const child = this.processState(context, state, target, args, io);
5719
+ const child = this.processState(context, state, io, target, args);
4114
5720
  child.errexit = errexit;
4115
5721
  child.braceexpand = braceexpand;
4116
5722
  if (direct)
4117
5723
  child.profile = interpreterProfile ?? state.profile ?? "bash";
4118
- const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: target });
4119
- let status = 0;
4120
- for (const unit of units) {
4121
- for (const warning of unit.warnings ?? [])
4122
- await writeDiagnostic(context.stderr, `${target}: warning: ${warning}\n`);
4123
- if (!unit.lists.length)
4124
- continue;
4125
- const result = await this.runUnit(unit, child, childIO);
4126
- status = result.exitCode;
4127
- if (result.terminated)
4128
- break;
5724
+ const references = new PipeDescriptorFrame(io[invocationScope]);
5725
+ const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, assignmentDiagnosticContext: undefined, scriptName: target }, references);
5726
+ try {
5727
+ let status = 0;
5728
+ for (const unit of units) {
5729
+ for (const warning of unit.warnings ?? [])
5730
+ await writeDiagnostic(context.stderr, `${target}: warning: ${warning}\n`);
5731
+ if (!unit.lists.length)
5732
+ continue;
5733
+ const result = await this.runUnit(unit, child, childIO);
5734
+ status = result.exitCode;
5735
+ if (result.terminated)
5736
+ break;
5737
+ }
5738
+ return await this.finishShell(child, childIO, status);
5739
+ }
5740
+ finally {
5741
+ await references.close();
4129
5742
  }
4130
- return status;
4131
5743
  }
4132
- async runCurrentText(source, state, io, fatalSyntax, syntaxName, sourceValues) {
5744
+ async runCurrentText(source, state, io, fatalSyntax, syntaxName, byteSource = false, includeSyntaxContext = true, sourceValues) {
4133
5745
  const lineIndex = new SourceLineIndex(source, this.budget.parsing);
4134
5746
  let position = 0;
4135
5747
  let status = 0;
@@ -4137,11 +5749,11 @@ export class Runtime {
4137
5749
  try {
4138
5750
  do {
4139
5751
  this.signal.throwIfAborted();
4140
- const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing, lineIndex, sourceValues);
5752
+ const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing, lineIndex, sourceValues, byteSource, state.extensions?.syntax);
4141
5753
  for (const warning of unit.script.warnings ?? [])
4142
5754
  await writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: warning: ${warning}\n`);
4143
5755
  if (unit.script.lists.length) {
4144
- status = await this.script(unit.script, state, io);
5756
+ status = await this.inputUnit(unit.script, state, io);
4145
5757
  executed = true;
4146
5758
  }
4147
5759
  position = unit.next;
@@ -4151,7 +5763,7 @@ export class Runtime {
4151
5763
  catch (error) {
4152
5764
  if (!(error instanceof ShellSyntaxError))
4153
5765
  throw error;
4154
- const status = await this.syntaxFailure(error, source, syntaxName === undefined ? io : { ...io, scriptName: syntaxName }, false);
5766
+ const status = await this.syntaxFailure(error, source, syntaxName === undefined ? io : { ...io, scriptName: syntaxName }, false, includeSyntaxContext);
4155
5767
  if (fatalSyntax && !executed)
4156
5768
  throw new Flow("exit", status);
4157
5769
  return status;
@@ -4195,7 +5807,7 @@ export class Runtime {
4195
5807
  throw error;
4196
5808
  }
4197
5809
  try {
4198
- return await this.runCurrentText(source.text, state, { ...io, diagnosticOffset: (io.diagnosticLine ?? 1) - 1 }, special, `${io.scriptName ?? "shell"}: eval`, source.values);
5810
+ return await this.runCurrentText(source.text, state, { ...io, diagnosticOffset: (io.diagnosticLine ?? 1) - 1, assignmentDiagnosticContext: { name: context.command } }, special, `${io.scriptName ?? "shell"}: eval`, false, true, source.values);
4199
5811
  }
4200
5812
  finally {
4201
5813
  const restore = () => { state.depth--; };
@@ -4314,15 +5926,38 @@ export class Runtime {
4314
5926
  restoration?.close();
4315
5927
  throw error;
4316
5928
  }
5929
+ let primaryFailure = false;
5930
+ let outcome;
5931
+ let leaveFailure;
4317
5932
  try {
4318
- return await this.runCurrentText(source, state, { ...io, scriptName: target, diagnosticOffset: 0, diagnosticLine: 1 }, special);
5933
+ try {
5934
+ await this.extensionEvent("source-enter", state, io, state.status);
5935
+ const status = await this.runCurrentText(source, state, { ...io, scriptName: target, diagnosticOffset: 0, diagnosticLine: 1, assignmentDiagnosticContext: undefined }, special);
5936
+ outcome = { kind: "return", value: await this.finishReturn("source-return", state, io, status) };
5937
+ }
5938
+ catch (error) {
5939
+ if (error instanceof Flow && error.kind === "return")
5940
+ outcome = { kind: "return", value: await this.finishReturn("source-return", state, io, error.status, error.previousStatus) };
5941
+ else
5942
+ throw error;
5943
+ }
4319
5944
  }
4320
5945
  catch (error) {
4321
- if (error instanceof Flow && error.kind === "return")
4322
- return error.status;
4323
- throw error;
5946
+ primaryFailure = !(error instanceof Flow) || error instanceof NounsetDiagnosticFailure;
5947
+ outcome = { kind: "throw", reason: error instanceof Flow ? error : new NounsetDiagnosticFailure(error) };
4324
5948
  }
4325
5949
  finally {
5950
+ const scope = io[invocationScope];
5951
+ try {
5952
+ if (!this.signal.aborted)
5953
+ await this.extensionEvent("source-leave", state, io, state.status);
5954
+ }
5955
+ catch (reason) {
5956
+ if (primaryFailure)
5957
+ scope.failures.push(reason);
5958
+ else
5959
+ leaveFailure = { reason };
5960
+ }
4326
5961
  const restore = () => {
4327
5962
  state.depth--;
4328
5963
  state.sourceDepth = sourceDepth;
@@ -4339,10 +5974,29 @@ export class Runtime {
4339
5974
  else
4340
5975
  restore();
4341
5976
  }
4342
- finally {
5977
+ catch (error) {
5978
+ scope.failures.push(error);
5979
+ }
5980
+ try {
4343
5981
  savedPositionals?.close();
4344
5982
  }
5983
+ catch (error) {
5984
+ scope.failures.push(error);
5985
+ }
5986
+ try {
5987
+ restoration?.close();
5988
+ }
5989
+ catch (error) {
5990
+ scope.failures.push(error);
5991
+ }
5992
+ }
5993
+ if (leaveFailure) {
5994
+ this.signal.throwIfAborted();
5995
+ throw leaveFailure.reason instanceof Flow ? leaveFailure.reason : new NounsetDiagnosticFailure(leaveFailure.reason);
4345
5996
  }
5997
+ if (outcome.kind === "throw")
5998
+ throw outcome.reason;
5999
+ return outcome.value;
4346
6000
  }
4347
6001
  invoke(name, args, options = {}, context, state, parent) {
4348
6002
  return this.invokeChild(options, state, parent, () => {
@@ -4360,6 +6014,7 @@ export class Runtime {
4360
6014
  scope.register(() => allocation.close());
4361
6015
  const carrier = this.admitArguments(getCommandArguments({ args, ...(options.argumentValues ? { argumentValues: options.argumentValues } : {}) }).values, allocation);
4362
6016
  const child = await cloneState(state, this.signal, scope, false);
6017
+ child.extensions = forkExtensions(state.extensions, "invocation");
4363
6018
  child.cwd = resolvePath(context.cwd, options.cwd ?? ".");
4364
6019
  const env = options.replaceEnv ? { ...options.env } : { ...context.env, ...options.env, PWD: child.cwd };
4365
6020
  if (guestArrays(child) || Object.keys(env).some(key => arrayStore(child)?.get(key)))
@@ -4382,14 +6037,15 @@ export class Runtime {
4382
6037
  child.locals = [];
4383
6038
  const input = options.stdin === undefined ? undefined : new ShellInput(options.stdin, this.budget, this.signal);
4384
6039
  const stdinIsDefault = options.stdin === undefined ? context.stdinIsDefault : (options.stdinIsDefault ?? false);
4385
- const io = {
6040
+ const references = new PipeDescriptorFrame(scope);
6041
+ const io = isolateIO({
4386
6042
  ...context,
4387
6043
  [invocationScope]: scope,
4388
6044
  stdin: input ?? context.stdin,
4389
6045
  ...(stdinIsDefault === undefined ? {} : { stdinIsDefault }),
4390
6046
  stdout: options.stdout ? this.budget.sink(options.stdout, this.signal) : context.stdout,
4391
6047
  stderr: options.stderr ? this.budget.sink(options.stderr, this.signal) : context.stderr,
4392
- };
6048
+ }, references);
4393
6049
  const command = {
4394
6050
  kind: "simple", redirects: [],
4395
6051
  words: [name, ...carrier.values].map((value) => {
@@ -4403,8 +6059,18 @@ export class Runtime {
4403
6059
  return { exitCode: await this.runCommandIsolated(command, child, io) };
4404
6060
  }
4405
6061
  finally {
4406
- stateMonitor(child)?.closeValues();
4407
- await input?.close();
6062
+ try {
6063
+ await this.releaseExtensions(child);
6064
+ }
6065
+ finally {
6066
+ try {
6067
+ stateMonitor(child)?.closeValues();
6068
+ await input?.close();
6069
+ }
6070
+ finally {
6071
+ await references.close();
6072
+ }
6073
+ }
4408
6074
  }
4409
6075
  }
4410
6076
  async setOptions(context, state) {
@@ -4436,12 +6102,17 @@ export class Runtime {
4436
6102
  state.errexit = enabled;
4437
6103
  else if (flag === "u")
4438
6104
  state.nounset = enabled;
6105
+ else if ([...state.extensions?.options.values() ?? []].some(option => option.flag === flag)) {
6106
+ for (const option of state.extensions.options.values())
6107
+ if (option.flag === flag)
6108
+ option.enabled = enabled;
6109
+ }
4439
6110
  else if (flag === "B")
4440
6111
  state.braceexpand = enabled;
4441
6112
  else if (flag === "o" && position === option.length - 1) {
4442
6113
  const name = args[index + 1];
4443
6114
  if (name === undefined) {
4444
- const options = [["braceexpand", state.braceexpand !== false], ["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail]];
6115
+ const options = [["braceexpand", state.braceexpand !== false], ["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail], ...[...state.extensions?.options.values() ?? []].map(option => [option.name, option.enabled])];
4445
6116
  for (const [name, active] of options)
4446
6117
  await writeText(stdout, enabled ? `${name}\t${active ? "on" : "off"}\n` : `set ${active ? "-" : "+"}o ${name}\n`);
4447
6118
  }
@@ -4454,6 +6125,8 @@ export class Runtime {
4454
6125
  state.pipefail = enabled;
4455
6126
  else if (name === "braceexpand")
4456
6127
  state.braceexpand = enabled;
6128
+ else if (state.extensions?.options.has(name))
6129
+ state.extensions.options.get(name).enabled = enabled;
4457
6130
  else {
4458
6131
  const unsupported = unsupportedSetOptionNames.has(name);
4459
6132
  await this.diagnostic(context, `set: ${name}: ${unsupported ? "unsupported shell option" : "invalid option name"}`);
@@ -4886,13 +6559,27 @@ export class Runtime {
4886
6559
  for (const name of ["dotglob", "globstar"])
4887
6560
  if ((!set || state[name]) && (!unset || !state[name]))
4888
6561
  await emit(name);
6562
+ for (const option of state.extensions?.shoptOptions.values() ?? [])
6563
+ if (!quiet && (!set || option.enabled) && (!unset || !option.enabled))
6564
+ await writeText(context.stdout, print ? `shopt -${option.enabled ? "s" : "u"} ${option.name}\n` : `${option.name.padEnd(19)}\t${option.enabled ? "on" : "off"}\n`);
4889
6565
  return 0;
4890
6566
  }
4891
6567
  let status = 0;
4892
6568
  for (; index < context.args.length; index++) {
4893
6569
  this.signal.throwIfAborted();
4894
6570
  const name = context.args[index];
4895
- if (name !== "dotglob" && name !== "globstar") {
6571
+ const extension = state.extensions?.shoptOptions.get(name);
6572
+ if (extension) {
6573
+ if (set || unset)
6574
+ extension.enabled = set;
6575
+ else {
6576
+ if (!quiet)
6577
+ await writeText(context.stdout, print ? `shopt -${extension.enabled ? "s" : "u"} ${name}\n` : `${name.padEnd(19)}\t${extension.enabled ? "on" : "off"}\n`);
6578
+ if (!extension.enabled)
6579
+ status = 1;
6580
+ }
6581
+ }
6582
+ else if (name !== "dotglob" && name !== "globstar") {
4896
6583
  await this.diagnostic(context, `shopt: ${name}: unsupported shell option name (supported: dotglob, globstar)`);
4897
6584
  status = 1;
4898
6585
  }
@@ -4906,6 +6593,54 @@ export class Runtime {
4906
6593
  }
4907
6594
  return status;
4908
6595
  }
6596
+ async printDeclarationValue(value, state, io) {
6597
+ const allocation = this.budget.values.scope();
6598
+ try {
6599
+ allocation.hold(value);
6600
+ const length = shellValueByteLength(value);
6601
+ allocation.reserve(length * 8 + 16, 1);
6602
+ const bytes = shellValueBytes(value, allocation);
6603
+ let text;
6604
+ if (!byteLocale(state.variables)) {
6605
+ try {
6606
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
6607
+ }
6608
+ catch {
6609
+ text = undefined;
6610
+ }
6611
+ }
6612
+ const ansi = text === undefined ? bytes.some(byte => byte < 32 || byte >= 127) : /[\p{C}\p{Zl}\p{Zp}]/u.test(text);
6613
+ let quoted = ansi ? "$'" : '"';
6614
+ const controls = ["", "", "", "", "", "", "", "\\a", "\\b", "\\t", "\\n", "\\v", "\\f", "\\r"];
6615
+ let visited = 0;
6616
+ for (const item of text ?? bytes) {
6617
+ if (visited++ % 1024 === 0) {
6618
+ this.budget.cpuCheckpoint();
6619
+ await yieldTurn(this.signal);
6620
+ }
6621
+ const character = typeof item === "number" ? String.fromCharCode(item) : item;
6622
+ const code = character.codePointAt(0);
6623
+ if (ansi && controls[code])
6624
+ quoted += controls[code];
6625
+ else if (ansi && code === 27)
6626
+ quoted += "\\E";
6627
+ else if (ansi && (code < 32 || code === 127 || text === undefined && code >= 128 || /[\p{C}\p{Zl}\p{Zp}]/u.test(character))) {
6628
+ const encoded = text === undefined ? [code] : new TextEncoder().encode(character);
6629
+ for (const byte of encoded)
6630
+ quoted += `\\${byte.toString(8).padStart(3, "0")}`;
6631
+ }
6632
+ else if (character === "\\" || (ansi ? character === "'" : character === '"' || character === "$" || character === "`"))
6633
+ quoted += `\\${character}`;
6634
+ else
6635
+ quoted += character;
6636
+ }
6637
+ quoted += ansi ? "'" : '"';
6638
+ await writeText(io.stdout, quoted);
6639
+ }
6640
+ finally {
6641
+ allocation.close();
6642
+ }
6643
+ }
4909
6644
  async printfVariable(context, state, assignments) {
4910
6645
  const incoming = getCommandArguments(context);
4911
6646
  let offset = 0;
@@ -5163,118 +6898,12 @@ export class Runtime {
5163
6898
  }
5164
6899
  }
5165
6900
  }
5166
- async waitBuiltin(context, state) {
5167
- let next = false;
5168
- let variable;
5169
- let index = 0;
5170
- for (; index < context.args.length; index++) {
5171
- const option = context.args[index];
5172
- if (option === "--") {
5173
- index++;
5174
- break;
5175
- }
5176
- if (!option.startsWith("-") || option === "-")
5177
- break;
5178
- for (let flag = 1; flag < option.length; flag++) {
5179
- const letter = option[flag];
5180
- if (letter === "n")
5181
- next = true;
5182
- else if (letter === "f") { /* Processes do not stop in the virtual shell. */ }
5183
- else if (letter === "p") {
5184
- variable = option.slice(flag + 1) || context.args[++index];
5185
- if (variable === undefined) {
5186
- await writeDiagnostic(context.stderr, "wait: -p: option requires an argument\n");
5187
- return 2;
5188
- }
5189
- if (!/^[a-zA-Z_][a-zA-Z_0-9]*(?:\[.+\])?$/su.test(variable)) {
5190
- await writeDiagnostic(context.stderr, `wait: ${variable}: not a valid identifier\n`);
5191
- return 1;
5192
- }
5193
- break;
5194
- }
5195
- else {
5196
- await writeDiagnostic(context.stderr, `wait: -${letter}: invalid option\n`);
5197
- return 2;
5198
- }
5199
- }
5200
- }
5201
- const target = variable === undefined ? undefined : /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(.+)\])?$/su.exec(variable);
5202
- if (target) {
5203
- const name = target[1];
5204
- if (state.readonlyVariables?.has(name)) {
5205
- await writeDiagnostic(context.stderr, `wait: ${name}: readonly variable\n`);
5206
- return 1;
5207
- }
5208
- if (target[2] === undefined) {
5209
- if (arrayStore(state)?.get(name))
5210
- await this.unsetIndexed(state, name);
5211
- else
5212
- this.unsetVariable(state, name);
5213
- }
5214
- }
5215
- const publish = async (id) => {
5216
- if (!target)
5217
- return;
5218
- if (target[2] === undefined)
5219
- await this.assignVariable(state, target[1], id);
5220
- else
5221
- await this.arrayAssignment({ kind: "element", name: target[1], index: { decimal: target[2], source: target[2] }, append: false, value: { offset: 0, parts: [{ kind: "text", value: id, quoted: true }] } }, state, context);
5222
- };
5223
- const table = state.backgroundJobs;
5224
- const ids = context.args.slice(index);
5225
- const selected = [];
5226
- let invalid = false;
5227
- for (const id of ids) {
5228
- if (!id.startsWith("%") && !/^[0-9]+$/u.test(id)) {
5229
- await writeDiagnostic(context.stderr, `wait: ${id}: not a pid or valid job spec\n`);
5230
- if (next)
5231
- continue;
5232
- return 1;
5233
- }
5234
- const job = table.resolve(id);
5235
- if (!job) {
5236
- await writeDiagnostic(context.stderr, `wait: ${id}: not a child of this shell\n`);
5237
- invalid = true;
5238
- }
5239
- else
5240
- selected.push(job);
5241
- }
5242
- if (next) {
5243
- const eligible = ids.length ? selected.filter(job => !job.waited) : [...table.jobs.values()].filter(job => !job.done && !job.consumed);
5244
- if (!eligible.length)
5245
- return 127;
5246
- const result = await interruptible(Promise.race(eligible.map(async (job) => ({ job, status: await job.promise }))), this.signal);
5247
- result.job.consumed = true;
5248
- await publish(result.job.id);
5249
- return result.status;
5250
- }
5251
- if (!ids.length) {
5252
- await interruptible(Promise.all([...table.jobs.values()].map(job => job.promise)), this.signal);
5253
- table.jobs.clear();
5254
- return 0;
5255
- }
5256
- let status = invalid ? 127 : 0;
5257
- for (const id of ids) {
5258
- const job = table.resolve(id);
5259
- if (!job) {
5260
- status = 127;
5261
- continue;
5262
- }
5263
- status = await interruptible(job.promise, this.signal);
5264
- job.consumed = true;
5265
- job.waited = true;
5266
- await publish(job.id);
5267
- }
5268
- return status;
5269
- }
5270
6901
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
5271
6902
  const { command, args, stdout, stderr } = context;
5272
6903
  if (command === ":" || command === "true")
5273
6904
  return 0;
5274
6905
  if (command === "false")
5275
6906
  return 1;
5276
- if (command === "wait")
5277
- return this.waitBuiltin(context, state);
5278
6907
  if (command === "shopt")
5279
6908
  return this.shoptBuiltin(context, state);
5280
6909
  if (command === "let")
@@ -5358,6 +6987,8 @@ export class Runtime {
5358
6987
  const associativeDeclaration = command === "declare";
5359
6988
  const declarationArgs = [...args];
5360
6989
  let indexedLocal = associativeDeclaration;
6990
+ const readonlySyntax = state.extensions?.syntax.indexedDeclarations?.includes("readonly") === true;
6991
+ let indexedReadonly = false;
5361
6992
  if (associativeDeclaration) {
5362
6993
  if (declarationArgs.shift() !== "-A" || !declarationArgs.length) {
5363
6994
  await this.diagnostic(context, "declare: only -A NAME is supported");
@@ -5382,9 +7013,26 @@ export class Runtime {
5382
7013
  const option = declarationArgs.shift();
5383
7014
  if (option === "--")
5384
7015
  break;
5385
- if (option !== "-p") {
5386
- await writeDiagnostic(stderr, `readonly: ${option}: unsupported option\n`);
5387
- return 2;
7016
+ if (!readonlySyntax) {
7017
+ if (option !== "-p") {
7018
+ await writeDiagnostic(stderr, `readonly: ${option}: unsupported option\n`);
7019
+ return 2;
7020
+ }
7021
+ }
7022
+ else {
7023
+ if (option === "-") {
7024
+ declarationArgs.unshift(option);
7025
+ break;
7026
+ }
7027
+ for (const flag of option.slice(1)) {
7028
+ if (flag === "a")
7029
+ indexedReadonly = true;
7030
+ else if (flag !== "p") {
7031
+ await this.diagnostic(context, `readonly: -${flag}: invalid option`);
7032
+ await writeText(stderr, "readonly: usage: readonly [-aAf] [name[=value] ...] or readonly -p\n");
7033
+ return 2;
7034
+ }
7035
+ }
5388
7036
  }
5389
7037
  }
5390
7038
  }
@@ -5396,6 +7044,48 @@ export class Runtime {
5396
7044
  let status = 0;
5397
7045
  if (!declarationArgs.length) {
5398
7046
  const names = command === "readonly" ? state.readonlyVariables ?? [] : state.exported;
7047
+ if (command === "readonly" && readonlySyntax) {
7048
+ const allocation = this.budget.values.scope();
7049
+ try {
7050
+ const count = state.readonlyVariables?.size ?? 0;
7051
+ allocation.reserve(count * 32, count);
7052
+ for (const name of [...names].sort()) {
7053
+ this.signal.throwIfAborted();
7054
+ const binding = arrayStore(state)?.get(name);
7055
+ if (indexedReadonly && !binding)
7056
+ continue;
7057
+ if (binding) {
7058
+ binding.retain();
7059
+ try {
7060
+ allocation.reserve(binding.values.size * 16, binding.values.size);
7061
+ await writeText(stdout, `declare -ar ${name}=(`);
7062
+ let first = true;
7063
+ for (const index of [...binding.values.keys()].sort((left, right) => left - right)) {
7064
+ await writeText(stdout, `${first ? "" : " "}[${index}]=`);
7065
+ await this.printDeclarationValue(binding.getValue(index), state, context);
7066
+ first = false;
7067
+ }
7068
+ await writeText(stdout, ")\n");
7069
+ }
7070
+ finally {
7071
+ await binding.release();
7072
+ }
7073
+ }
7074
+ else {
7075
+ await writeText(stdout, `declare -r ${name}`);
7076
+ if (Object.hasOwn(state.variables, name)) {
7077
+ await writeText(stdout, "=");
7078
+ await this.printDeclarationValue(stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], state, context);
7079
+ }
7080
+ await writeText(stdout, "\n");
7081
+ }
7082
+ }
7083
+ }
7084
+ finally {
7085
+ allocation.close();
7086
+ }
7087
+ return 0;
7088
+ }
5399
7089
  for (const name of names)
5400
7090
  if (arrayStore(state)?.get(name)) {
5401
7091
  await this.diagnostic(context, "indexed array: listing indexed bindings is unsupported");
@@ -5409,15 +7099,21 @@ export class Runtime {
5409
7099
  const declarationOffset = args.length - declarationArgs.length;
5410
7100
  for (let declarationIndex = 0; declarationIndex < declarationArgs.length; declarationIndex++) {
5411
7101
  const arg = declarationArgs[declarationIndex];
5412
- const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:=(.*))?$/su.exec(arg);
7102
+ const match = (command === "readonly" && readonlySyntax ? /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\+?=(.*))?$/su : /^([a-zA-Z_][a-zA-Z_0-9]*)(?:=(.*))?$/su).exec(arg);
5413
7103
  if (!match) {
5414
7104
  await this.diagnostic(context, `${command}: \`${arg}': not a valid identifier`);
5415
7105
  status = 1;
5416
7106
  continue;
5417
7107
  }
5418
7108
  const name = match[1];
7109
+ const append = command === "readonly" && readonlySyntax && arg[name.length] === "+";
7110
+ const assignedValue = () => {
7111
+ const original = declarationValues[declarationOffset + declarationIndex];
7112
+ return typeof original === "string" ? match[2] : shellValueFromBytes(shellValueBytes(original, context[valueScope]).subarray(name.length + (append ? 2 : 1)), context[valueScope]);
7113
+ };
5419
7114
  if (state.readonlyVariables?.has(name) && (match[2] !== undefined || command === "local")) {
5420
- await this.diagnostic(context, `${name}: readonly variable`);
7115
+ const provenance = readonlySyntax && (command === "readonly" || command === "local" && indexedLocal) ? `${command}: ` : "";
7116
+ await this.diagnostic(context, `${provenance}${name}: readonly variable`);
5421
7117
  status = 1;
5422
7118
  continue;
5423
7119
  }
@@ -5426,6 +7122,32 @@ export class Runtime {
5426
7122
  status = 1;
5427
7123
  continue;
5428
7124
  }
7125
+ if (command === "readonly" && readonlySyntax && (indexedReadonly || match[2]?.startsWith("("))) {
7126
+ const assigned = match[2] !== undefined ? assignedValue() : undefined;
7127
+ if (match[2]?.startsWith("(")) {
7128
+ this.budget.source(shellValueByteLength(assigned));
7129
+ const source = typeof assigned === "string" ? assigned : Buffer.from(shellValueBytes(assigned, context[valueScope])).toString("latin1");
7130
+ const entries = parseCompoundArrayValue(source, byteLocale(state.variables), typeof assigned !== "string", state.extensions?.syntax, this.budget.parsing);
7131
+ await this.arrayAssignment({ kind: "compound", name, append, entries }, state, context, "readonly");
7132
+ }
7133
+ else if (assigned !== undefined) {
7134
+ await this.arrayAssignment({ kind: "element", name, append, index: { decimal: "0" }, value: {
7135
+ offset: 0, parts: [{ kind: "text", value: shellValueText(assigned), quoted: true, ...(typeof assigned === "string" ? {} : { byteValue: assigned }) }],
7136
+ } }, state, context, "readonly");
7137
+ }
7138
+ else if (!arrayStore(state)?.get(name) && Object.hasOwn(state.variables, name) && !state.readonlyVariables?.has(name)) {
7139
+ await this.arrayAssignment({ kind: "compound", name, append: true, entries: [] }, state, context, "readonly");
7140
+ }
7141
+ else {
7142
+ state.readonlyVariables ??= new Set();
7143
+ state.readonlyVariables.add(name);
7144
+ }
7145
+ const previous = assignments.get(name);
7146
+ if (previous && locals?.get(name) !== previous)
7147
+ previous.heldValue?.release();
7148
+ assignments.delete(name);
7149
+ continue;
7150
+ }
5429
7151
  if ((command === "local" || associativeDeclaration) && indexedLocal) {
5430
7152
  if (controlNames.has(name))
5431
7153
  throw new ArrayFailure("control binding cannot be indexed");
@@ -5460,10 +7182,9 @@ export class Runtime {
5460
7182
  if (!saved && current && current.associative !== associativeDeclaration)
5461
7183
  throw new ArrayFailure("cannot convert array kind");
5462
7184
  shadow = !saved && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner, associativeDeclaration);
5463
- const value = match[2] ?? (!saved && !current && Object.hasOwn(state.variables, name) ? state.variables[name] : undefined);
7185
+ const value = match[2] !== undefined ? assignedValue() : !saved && !current && Object.hasOwn(state.variables, name) ? stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name] : undefined;
5464
7186
  if (value !== undefined) {
5465
- const retained = associativeDeclaration && match[2] === undefined ? stateMonitor(state).values.get(name, value) : value;
5466
- const token = await valueToken(shadow.owner, retained, this.signal);
7187
+ const token = await textToken(shadow.owner, value, this.signal);
5467
7188
  try {
5468
7189
  shadow.insert(associativeDeclaration ? (await shadow.keyIndex("0", operation, this.signal, true)) : 0, token);
5469
7190
  }
@@ -5565,7 +7286,7 @@ export class Runtime {
5565
7286
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
5566
7287
  shadow = IndexedBinding.create(store.owner);
5567
7288
  if (match[2] !== undefined) {
5568
- const token = await textToken(shadow.owner, match[2], this.signal);
7289
+ const token = await textToken(shadow.owner, assignedValue(), this.signal);
5569
7290
  try {
5570
7291
  shadow.insert(0, token);
5571
7292
  }
@@ -5614,14 +7335,12 @@ export class Runtime {
5614
7335
  }
5615
7336
  }
5616
7337
  if (match[2] !== undefined && arrayStore(state)?.get(name)) {
5617
- await this.arrayZero(state, name, async () => match[2], false, command === "readonly");
7338
+ await this.arrayZero(state, name, async () => assignedValue(), append, command === "readonly");
5618
7339
  assignments.delete(name);
5619
7340
  continue;
5620
7341
  }
5621
7342
  if (match[2] !== undefined) {
5622
- const original = declarationValues[declarationOffset + declarationIndex];
5623
- const value = typeof original === "string" ? match[2] : shellValueFromBytes(shellValueBytes(original, context[valueScope]).subarray(name.length + 1), context[valueScope]);
5624
- this.writeVariable(state, name, value);
7343
+ this.writeVariable(state, name, append ? concatShellValues([stateMonitor(state)?.values.get(name, state.variables[name] ?? "") ?? state.variables[name] ?? "", assignedValue()], context[valueScope]) : assignedValue());
5625
7344
  }
5626
7345
  else if (command === "local" && name === "OPTIND")
5627
7346
  this.syncGetopts(state);
@@ -5647,7 +7366,7 @@ export class Runtime {
5647
7366
  const base = selected[1];
5648
7367
  const selector = selected[2];
5649
7368
  if (state.readonlyVariables?.has(base)) {
5650
- await this.diagnostic(context, "indexed array: readonly binding");
7369
+ await this.diagnostic(context, state.extensions?.syntax.indexedDeclarations?.includes("readonly") ? `unset: ${base}: cannot unset: readonly variable` : "indexed array: readonly binding");
5651
7370
  status = 1;
5652
7371
  continue;
5653
7372
  }
@@ -5758,58 +7477,34 @@ export class Runtime {
5758
7477
  return 2;
5759
7478
  }
5760
7479
  const input = context.stdin instanceof ShellInput ? context.stdin : new ShellInput(context.stdin, this.budget, this.signal);
5761
- const line = count === 0 && context.stdin === closedSource ? { value: "", escaped: new Set(), terminated: false }
5762
- : await input.line(raw, count === undefined && delimiter === undefined ? undefined : {
7480
+ const line = count === 0 && context.stdin === closedSource ? undefined
7481
+ : await input.line(raw, {
5763
7482
  ...(count === undefined ? {} : { count }), ...(delimiter === undefined ? {} : { delimiter }), byteCount: byteLocale(state.variables), exact,
5764
7483
  });
5765
- if (!names.length) {
5766
- if (state.readonlyVariables?.has("REPLY")) {
5767
- await this.diagnostic(context, "REPLY: readonly variable");
5768
- return 1;
7484
+ try {
7485
+ if (!names.length) {
7486
+ if (state.readonlyVariables?.has("REPLY")) {
7487
+ await this.diagnostic(context, "REPLY: readonly variable");
7488
+ return 1;
7489
+ }
7490
+ this.writeVariable(state, "REPLY", line?.shellValue ?? "");
7491
+ }
7492
+ else {
7493
+ const separators = exact ? "" : stateMonitor(state)?.values.get("IFS", state.variables.IFS ?? " \t\n") ?? state.variables.IFS ?? " \t\n";
7494
+ const fields = await line?.fields(separators, names.length) ?? [];
7495
+ for (let index = 0; index < names.length; index++) {
7496
+ if (state.readonlyVariables?.has(names[index])) {
7497
+ await this.diagnostic(context, `${names[index]}: readonly variable`);
7498
+ return index === names.length - 1 ? 1 : 2;
7499
+ }
7500
+ await this.assignVariable(state, names[index], fields[index]?.value ?? "");
7501
+ }
5769
7502
  }
5770
- this.writeVariable(state, "REPLY", line.value);
7503
+ return line?.terminated ? 0 : 1;
7504
+ }
7505
+ finally {
7506
+ line?.release();
5771
7507
  }
5772
- else {
5773
- const separators = exact ? "" : state.variables.IFS ?? " \t\n";
5774
- let end = 0;
5775
- let offset = 0;
5776
- let point = 0;
5777
- for (const character of line.value) {
5778
- offset += character.length;
5779
- if (line.escaped.has(point) || !separators.includes(character) || !" \t\n".includes(character))
5780
- end = offset;
5781
- point++;
5782
- }
5783
- let position = 0;
5784
- point = 0;
5785
- const separator = () => position < end && !line.escaped.has(point) && separators.includes(String.fromCodePoint(line.value.codePointAt(position)));
5786
- const whitespace = () => separator() && " \t\n".includes(line.value[position]);
5787
- const advance = () => { position += line.value.codePointAt(position) > 0xffff ? 2 : 1; point++; };
5788
- while (position < end && whitespace())
5789
- advance();
5790
- const fields = [];
5791
- while (position < end && fields.length < names.length) {
5792
- const start = position;
5793
- while (position < end && !separator())
5794
- advance();
5795
- fields.push({ start, end: position });
5796
- while (position < end && whitespace())
5797
- advance();
5798
- if (position < end && separator())
5799
- advance();
5800
- while (position < end && whitespace())
5801
- advance();
5802
- }
5803
- for (let index = 0; index < names.length; index++) {
5804
- if (state.readonlyVariables?.has(names[index])) {
5805
- await this.diagnostic(context, `${names[index]}: readonly variable`);
5806
- return index === names.length - 1 ? 1 : 2;
5807
- }
5808
- const field = fields[index];
5809
- await this.assignVariable(state, names[index], field ? line.value.slice(field.start, index === names.length - 1 && position < end ? end : field.end) : "");
5810
- }
5811
- }
5812
- return line.terminated ? 0 : 1;
5813
7508
  }
5814
7509
  if (command === "exit" || command === "return") {
5815
7510
  if (command === "return" && state.functionDepth === 0 && !state.sourceDepth) {
@@ -5825,7 +7520,7 @@ export class Runtime {
5825
7520
  throw completedExit(2, command);
5826
7521
  }
5827
7522
  const status = args[0] === undefined ? state.status : Number((BigInt(args[0]) % 256n + 256n) % 256n);
5828
- throw completedExit(status, command);
7523
+ throw completedExit(status, command, 1, state.status);
5829
7524
  }
5830
7525
  if (command === "break" || command === "continue") {
5831
7526
  const levels = args[0] === undefined ? 1 : Number(args[0]);
@@ -5844,14 +7539,23 @@ export class Runtime {
5844
7539
  async words(words, state, io, declaration = false) {
5845
7540
  return (await this.valueWords(words, state, io, declaration)).map(shellValueText);
5846
7541
  }
5847
- async valueWords(words, state, io, declaration = false) {
7542
+ async valueWords(words, state, io, declaration = false, indexedDeclaration = false) {
5848
7543
  const fields = [];
5849
7544
  for (const word of words) {
5850
- const values = await this.valueWord(word, state, io, !(declaration && this.assignment(word)), false, false, false, undefined, true);
5851
- if (values.length > this.budget.limits.maxExpansionFields - fields.length)
7545
+ const assignment = indexedDeclaration ? getArrayAssignment(word) : undefined;
7546
+ if (assignment) {
7547
+ await this.arrayAssignment(assignment, state, io, undefined, "declaration");
7548
+ fields.push(assignment.name);
7549
+ }
7550
+ else {
7551
+ const values = await this.valueWord(word, state, io, !(declaration && this.assignment(word)), false, false, false, undefined, false, true);
7552
+ if (values.length > this.budget.limits.maxExpansionFields - fields.length)
7553
+ this.budget.fail("maxExpansionFields");
7554
+ for (const value of values)
7555
+ fields.push(value);
7556
+ }
7557
+ if (fields.length > this.budget.limits.maxExpansionFields)
5852
7558
  this.budget.fail("maxExpansionFields");
5853
- for (const value of values)
5854
- fields.push(value);
5855
7559
  }
5856
7560
  return fields;
5857
7561
  }
@@ -5917,7 +7621,7 @@ export class Runtime {
5917
7621
  holding?.release();
5918
7622
  }
5919
7623
  }
5920
- async valuePart(part, state, io, hereString = false) {
7624
+ async valuePart(part, state, io, hereString = false, split = false, hereDocument = false) {
5921
7625
  if (part.kind === "variable" && part.transform) {
5922
7626
  const selector = getArraySelector(part);
5923
7627
  if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
@@ -5944,7 +7648,7 @@ export class Runtime {
5944
7648
  const existing = selector?.kind === "element" ? arrayStore(state)?.get(part.name)?.get(numericIndex(selector.index) ?? -1) ?? (numericIndex(selector.index) === 0 ? this.variable(state, part.name) : undefined)
5945
7649
  : /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name) ? this.variable(state, part.name)
5946
7650
  : /^[1-9][0-9]*$/u.test(part.name) ? state.positional[Number(part.name) - 1] : "";
5947
- const value = await this.valuePart(base, state, io, hereString);
7651
+ const value = await this.valuePart(base, state, io, hereString, split, hereDocument);
5948
7652
  return existing === undefined ? "" : this.transformValue(value, part.transform, state, io);
5949
7653
  }
5950
7654
  if (part.kind === "variable" && part.prefixNames) {
@@ -5973,24 +7677,24 @@ export class Runtime {
5973
7677
  const token = index === undefined ? undefined : binding.values.get(index)?.text;
5974
7678
  this.requireParameter(token?.value, part.kind === "variable" ? part.name : "", state, io);
5975
7679
  if (part.kind === "variable" && part.length)
5976
- return this.parameterLength(token?.value ?? "");
5977
- return token?.rawValue ? shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]) : token?.value ?? "";
7680
+ return this.valueLength(token?.shellValue ?? "", state, io);
7681
+ return token?.shellValue ? shellValueFromBytes(shellValueBytes(token.shellValue, io[valueScope]), io[valueScope]) : token?.value ?? "";
5978
7682
  }
5979
7683
  finally {
5980
7684
  holding?.release();
5981
7685
  }
5982
7686
  }
5983
- const index = selector?.kind === "element" ? numericIndex(selector.index) : binding?.associative ? binding.keys.get("30")?.index : 0;
5984
- const token = index === undefined || selector?.kind === "members" ? undefined : binding?.values.get(index)?.text;
7687
+ const index = selector?.kind === "element" ? numericIndex(selector.index, 4294967295) : binding?.associative ? binding.keys.get("30")?.index : 0;
7688
+ const token = index === undefined || selector && selector.kind !== "element" ? undefined : binding?.values.get(index)?.text;
5985
7689
  token?.retain();
5986
7690
  try {
5987
7691
  if (token && part.kind === "variable" && part.operator && ["#", "##", "%", "%%"].includes(part.operator))
5988
- return this.parameterPattern(part, token.rawValue ?? token.value, state, io, hereString);
5989
- if (token?.rawValue && part.kind === "variable" && !part.length && !part.operator && !part.substring)
5990
- return shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
5991
- const value = await this.partValue(part, state, io, hereString);
7692
+ return this.parameterPattern(part, token.shellValue, state, io, hereString);
7693
+ if (token?.shellValue && part.kind === "variable" && !part.length && !part.operator && !part.substring)
7694
+ return shellValueFromBytes(shellValueBytes(token.shellValue, io[valueScope]), io[valueScope]);
7695
+ const value = await this.partValue(part, state, io, hereString, split, hereDocument);
5992
7696
  if (binding)
5993
- await textToken(requireArrays(state).owner, shellValueText(value), this.signal);
7697
+ await textToken(requireArrays(state).owner, value, this.signal);
5994
7698
  return value;
5995
7699
  }
5996
7700
  finally {
@@ -6018,7 +7722,7 @@ export class Runtime {
6018
7722
  allocation.close();
6019
7723
  }
6020
7724
  }
6021
- async partValue(part, state, io, hereString) {
7725
+ async partValue(part, state, io, hereString, split, hereDocument) {
6022
7726
  this.signal.throwIfAborted();
6023
7727
  if (part.kind === "compound-substitution-eof") {
6024
7728
  await writeDiagnostic(io.stderr, `${io.scriptName ?? "shell"}: command substitution: line ${io.diagnosticLine ?? part.line}: syntax error: unexpected end of file\n`);
@@ -6052,12 +7756,16 @@ export class Runtime {
6052
7756
  if (part.kind === "substitution") {
6053
7757
  if (state.depth >= this.budget.limits.maxSubstitutionDepth)
6054
7758
  this.budget.fail("maxSubstitutionDepth");
7759
+ this.signal.throwIfAborted();
7760
+ if (part.form === "dollar-parenthesis" && state.extensions?.checkpoints.length)
7761
+ await this.extensionCheckpoint("source-input-read", state, io);
6055
7762
  const parameterDepth = io.parameterDepth ?? 0;
6056
7763
  if (parameterDepth > 0 && state.depth + parameterDepth + 1 > 64)
6057
7764
  throw new ShellSyntaxError("Syntax nesting exceeds 64", 0);
6058
7765
  const capture = new Capture();
6059
7766
  const child = await cloneState(state, this.signal);
6060
7767
  child.isolated = true;
7768
+ child.extensions = forkExtensions(state.extensions, "substitution");
6061
7769
  if (state.profile !== "sh")
6062
7770
  child.errexit = false;
6063
7771
  for (const [name, value] of state.redirectAssignments ?? []) {
@@ -6074,16 +7782,31 @@ export class Runtime {
6074
7782
  const substitutionDiagnosticLines = new Map();
6075
7783
  for (const [command, line] of part.script.printedLines ?? [])
6076
7784
  substitutionDiagnosticLines.set(command, part.sourceLine === undefined ? warningLine + (command.line ?? part.line) - part.line : warningLine + line - 1);
6077
- const resources = new BackgroundResources();
6078
- const captureIO = { ...isolateIO(io), [backgroundResources]: [...io[backgroundResources] ?? [], resources], substitutionDiagnosticLines, diagnosticOffset: (io.diagnosticLine ?? part.line) - (part.sourceLine ?? part.line), stdout: this.budget.sink(capture, this.signal) };
6079
- resources.destination = backgroundDestination(captureIO.stdout);
7785
+ const reprintedLines = part.sourceLine === undefined ? undefined : functionReprintedLines(part.script);
7786
+ const functionCommandLines = reprintedLines && new Map([...reprintedLines].map(([command, line]) => [command, warningLine + line - 1]));
7787
+ const references = new PipeDescriptorFrame(io[invocationScope]);
7788
+ let captured;
7789
+ const capturedOutput = new Promise(resolve => { captured = resolve; });
7790
+ const lifetime = new DescriptorLifetime(captured);
7791
+ const captureIO = isolateIO({
7792
+ ...io, substitutionDiagnosticLines,
7793
+ diagnosticOffset: (io.diagnosticLine ?? part.line) - (part.sourceLine ?? part.line),
7794
+ functionCommandLines, diagnosticCommandLines: undefined, stdout: this.budget.sink(capture, this.signal),
7795
+ }, references);
7796
+ captureIO.descriptors = new Map(captureIO.descriptors).set(1, { output: captureIO.stdout, lifetime });
6080
7797
  try {
6081
7798
  state.substitutionStatus = fileShortcut ? await this.runCommandIsolated(command, child, captureIO, true) : await this.run(part.script, child, captureIO);
6082
7799
  }
6083
7800
  finally {
6084
- await interruptible(resources.drain(), this.signal);
6085
- stateMonitor(child)?.closeValues();
7801
+ try {
7802
+ await references.close();
7803
+ await lifetime.release();
7804
+ }
7805
+ finally {
7806
+ stateMonitor(child)?.closeValues();
7807
+ }
6086
7808
  }
7809
+ await capturedOutput;
6087
7810
  state.status = state.substitutionStatus;
6088
7811
  const bytes = capture.bytes();
6089
7812
  if (bytes.includes(0))
@@ -6106,45 +7829,57 @@ export class Runtime {
6106
7829
  return shellValueFromBytes(sanitized, io[valueScope]);
6107
7830
  }
6108
7831
  }
7832
+ let specialValue;
7833
+ if (part.specialParameter) {
7834
+ const hook = state.extensions?.specialParameters.get(part.specialParameter.name);
7835
+ if (!hook)
7836
+ throw new TypeError("Missing captured shell special parameter handler");
7837
+ specialValue = hook.lookup(this.extensionContext(state, io));
7838
+ }
6109
7839
  const selector = getArraySelector(part);
6110
7840
  if (selector) {
6111
7841
  const store = requireArrays(state);
6112
7842
  const binding = store.get(part.name);
6113
7843
  if (selector.kind === "element") {
6114
- const index = numericIndex(selector.index);
7844
+ const index = numericIndex(selector.index, 4294967295);
6115
7845
  if (index === undefined)
6116
- throw new ArrayFailure("index outside 0..2147483647");
6117
- const value = binding ? binding.get(index) : index === 0 ? state.variables[part.name] : undefined;
6118
- this.requireParameter(value, `${part.name}[${selector.index}]`, state, io, part.line);
6119
- return part.length ? this.parameterLength(value ?? "") : value ?? "";
7846
+ throw new ArrayFailure("index outside 0..4294967295");
7847
+ const value = binding ? binding.getValue(index) : index === 0 && state.variables[part.name] !== undefined ? stateMonitor(state)?.values.get(part.name, state.variables[part.name]) ?? state.variables[part.name] : undefined;
7848
+ if (part.operator === "-" || part.operator === "+") {
7849
+ const alternate = part.operator === "+" ? value !== undefined : value === undefined;
7850
+ if (alternate)
7851
+ return concatShellValues(await this.valueWord(part.alternate, state, io, false, false, hereString, false, undefined, hereDocument), io[valueScope]);
7852
+ return part.operator === "+" ? "" : value ?? "";
7853
+ }
7854
+ this.requireParameter(value === undefined ? undefined : shellValueText(value), `${part.name}[${selector.index}]`, state, io, part.line);
7855
+ return part.length ? this.valueLength(value ?? "", state, io) : value ?? "";
6120
7856
  }
6121
7857
  if (part.length)
6122
7858
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
6123
- const values = await this.arrayMembers(part.name, state, io, part.keys);
6124
- const separator = Array.from(state.variables.IFS ?? " ")[0] ?? "";
6125
- if (values.every(value => typeof value === "string"))
6126
- return this.arrayJoin(store.owner, values, separator);
6127
- io[valueScope]?.reserve(values.length * 32 + 64, 0);
6128
- return concatShellValues(values.flatMap((value, index) => index ? [separator, value] : [value]), io[valueScope]);
6129
- }
6130
- let value = part.name === "!" ? state.lastBackgroundJob
7859
+ const values = await this.arrayMembers(part.name, state, io, selector.kind === "keys" || part.keys === true);
7860
+ const space = selector.kind === "keys" && (hereDocument || (selector.separator === "@"
7861
+ ? !part.quoted && !split || state.variables.IFS === ""
7862
+ : !part.quoted && split && state.variables.IFS === ""));
7863
+ return this.arrayJoin(store.owner, values, space ? " " : this.ifsSeparator(state, io));
7864
+ }
7865
+ let value = part.specialParameter ? specialValue === undefined ? undefined : shellValueText(specialValue)
6131
7866
  : part.name === "?" ? String(state.status)
6132
7867
  : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
6133
7868
  : part.name === "#" ? String(state.positional.length)
6134
7869
  : part.name === "@" || part.name === "*" ? state.positional.join(hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "")
6135
7870
  : /^0+$/u.test(part.name) ? state.arg0 ?? "virtual-bash"
6136
7871
  : /^\d+$/u.test(part.name) ? state.positional[Number(part.name) - 1]
6137
- : this.variable(state, part.name);
6138
- let retained = value;
6139
- if (!part.length && value !== undefined) {
6140
- if (/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name) && !arrayStore(state)?.get(part.name))
6141
- retained = stateMonitor(state)?.values.get(part.name, value) ?? value;
7872
+ : part.name === "LINENO" && state.extensions ? String(io.diagnosticLine ?? part.line ?? 1) : this.variable(state, part.name);
7873
+ let retained = part.specialParameter ? specialValue : value;
7874
+ if (value !== undefined) {
7875
+ if (/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name))
7876
+ retained = arrayStore(state)?.get(part.name)?.getValue(0) ?? stateMonitor(state)?.values.get(part.name, value) ?? value;
6142
7877
  else if (/^0+$/u.test(part.name))
6143
7878
  retained = stateMonitor(state)?.positionals.get(zeroPositionKey, value) ?? value;
6144
- else if (/^[1-9][0-9]*$/u.test(part.name))
7879
+ else if (/^[0-9]+$/u.test(part.name))
6145
7880
  retained = stateMonitor(state)?.positionals.get(String(Number(part.name) - 1), value) ?? value;
6146
7881
  else if (part.name === "@" || part.name === "*") {
6147
- const separator = hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "";
7882
+ const separator = hereString && (part.name === "@" || !part.quoted) ? " " : this.ifsSeparator(state, io);
6148
7883
  const values = this.positionalValues(state);
6149
7884
  retained = concatShellValues(values.flatMap((entry, index) => index ? [separator, entry] : [entry]), io[valueScope]);
6150
7885
  }
@@ -6166,13 +7901,13 @@ export class Runtime {
6166
7901
  if (operator === "=" && arrayStore(state)?.get(part.name)) {
6167
7902
  alternate = "";
6168
7903
  await this.arrayZero(state, part.name, async () => {
6169
- alternate = await this.arrayJoin(requireArrays(state).owner, await this.word(part.alternate, state, operandIO, false, false, hereString), "");
6170
- return alternate;
7904
+ retained = await this.arrayJoin(requireArrays(state).owner, await this.valueWord(part.alternate, state, operandIO, false, false, hereString, false, undefined, hereDocument), "");
7905
+ return retained;
6171
7906
  });
6172
- value = alternate;
6173
- return part.length ? this.parameterLength(value) : value;
7907
+ value = shellValueText(retained);
7908
+ return part.length ? this.valueLength(retained, state, io) : retained;
6174
7909
  }
6175
- retained = concatShellValues(await this.valueWord(part.alternate, state, operandIO, false, false, hereString), io[valueScope]);
7910
+ retained = concatShellValues(await this.valueWord(part.alternate, state, operandIO, false, false, hereString, false, undefined, hereDocument), io[valueScope]);
6176
7911
  alternate = shellValueText(retained);
6177
7912
  if (operator === "?")
6178
7913
  throw new ParameterExpansionFailure(`${part.name}: ${alternate || (part.operator.startsWith(":") ? "parameter null or not set" : "parameter not set")}`, io.diagnosticLine ?? part.line);
@@ -6190,15 +7925,39 @@ export class Runtime {
6190
7925
  }
6191
7926
  else
6192
7927
  this.requireParameter(value, part.name, state, io, part.line);
6193
- return part.length ? this.parameterLength(value ?? "") : retained ?? "";
7928
+ return part.length ? this.valueLength(retained ?? "", state, io) : retained ?? "";
6194
7929
  }
6195
- async parameterLength(value) {
7930
+ async valueLength(value, state, io) {
6196
7931
  const limit = this.budget.limits.maxExpansionBytes;
6197
7932
  const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
6198
- const scanned = await scanString(value, work);
6199
- if (scanned.bytes > limit)
7933
+ if (typeof value === "string") {
7934
+ const scanned = await scanString(value, work);
7935
+ if (scanned.bytes > limit)
7936
+ this.budget.fail("maxExpansionBytes");
7937
+ return String(byteLocale(state.variables) ? scanned.bytes : scanned.count);
7938
+ }
7939
+ if (shellValueByteLength(value) > limit)
6200
7940
  this.budget.fail("maxExpansionBytes");
6201
- return String(scanned.count);
7941
+ const bytes = shellValueBytes(value, io[valueScope]);
7942
+ if (byteLocale(state.variables))
7943
+ return String(bytes.length);
7944
+ let length = 0;
7945
+ for (let offset = 0; offset < bytes.length; offset += shellCharacterWidth(bytes, offset, false)) {
7946
+ length++;
7947
+ const pending = stringCheckpoint(work);
7948
+ if (pending)
7949
+ await pending;
7950
+ }
7951
+ return String(length);
7952
+ }
7953
+ ifsSeparator(state, io) {
7954
+ const text = state.variables.IFS ?? " ";
7955
+ const value = stateMonitor(state)?.values.get("IFS", text) ?? text;
7956
+ const byteCount = byteLocale(state.variables);
7957
+ if (typeof value === "string" && (!byteCount || !value || value.charCodeAt(0) < 128))
7958
+ return value ? String.fromCodePoint(value.codePointAt(0)) : "";
7959
+ const bytes = shellValueBytes(value, io[valueScope]);
7960
+ return bytes.length ? shellValueFromBytes(bytes.subarray(0, shellCharacterWidth(bytes, 0, byteCount)), io[valueScope]) : "";
6202
7961
  }
6203
7962
  async substring(part, value, state, io) {
6204
7963
  const owner = arrayStore(state)?.get(part.name) ? requireArrays(state).owner : undefined;
@@ -6422,12 +8181,12 @@ export class Runtime {
6422
8181
  async word(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
6423
8182
  return (await this.valueWord(word, state, io, split, pattern, hereString, conditionalPattern, regexAppend)).map(shellValueText);
6424
8183
  }
6425
- async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend, braces = split && !pattern && !hereString) {
8184
+ async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend, hereDocument = false, braces = split && !pattern && !hereString && !hereDocument) {
6426
8185
  if (braces && state.braceexpand !== false && word.parts.some(part => part.kind === "text" && !part.quoted && part.value.includes("{"))) {
6427
8186
  const fields = [];
6428
8187
  let bytes = 0;
6429
8188
  for await (const expanded of expandBraces(word, this.budget, this.signal)) {
6430
- const values = await this.valueWord(expanded, state, io, split, pattern, hereString, conditionalPattern, regexAppend, false);
8189
+ const values = await this.valueWord(expanded, state, io, split, pattern, hereString, conditionalPattern, regexAppend, hereDocument, false);
6431
8190
  if (values.length > this.budget.limits.maxExpansionFields - fields.length)
6432
8191
  this.budget.fail("maxExpansionFields");
6433
8192
  for (const value of values) {
@@ -6515,6 +8274,43 @@ export class Runtime {
6515
8274
  if (shellValueByteLength(value) === 0)
6516
8275
  return;
6517
8276
  const separators = state.variables.IFS ?? " \t\n";
8277
+ const retainedSeparators = stateMonitor(state)?.values.get("IFS", separators) ?? separators;
8278
+ let byteSeparators = false;
8279
+ if (byteLocale(state.variables) && typeof retainedSeparators === "string") {
8280
+ const work = this.splitWork ??= { scanned: 0 };
8281
+ for (let index = 0; index < retainedSeparators.length; index++) {
8282
+ if (retainedSeparators.charCodeAt(index) > 127) {
8283
+ byteSeparators = true;
8284
+ break;
8285
+ }
8286
+ if (++work.scanned >= 4096) {
8287
+ work.scanned = 0;
8288
+ await yieldTurn(this.signal);
8289
+ }
8290
+ }
8291
+ }
8292
+ if (typeof value !== "string" || typeof retainedSeparators !== "string" || byteSeparators) {
8293
+ let boundary = false;
8294
+ for await (const piece of this.splitRawValue(value, retainedSeparators, io, byteLocale(state.variables))) {
8295
+ if (piece.separator) {
8296
+ if (!piece.whitespace) {
8297
+ fields.at(-1).present = true;
8298
+ addField();
8299
+ }
8300
+ else if (fields.at(-1).present)
8301
+ boundary = true;
8302
+ }
8303
+ else {
8304
+ if (boundary)
8305
+ addField();
8306
+ boundary = false;
8307
+ append(piece.value, true, true);
8308
+ }
8309
+ }
8310
+ if (boundary)
8311
+ addField();
8312
+ return;
8313
+ }
6518
8314
  const separatorScope = this.budget.values.scope();
6519
8315
  try {
6520
8316
  separatorScope.reserve(64, 0);
@@ -6567,8 +8363,16 @@ export class Runtime {
6567
8363
  const { part, splitText, io: partIO } = parts[index];
6568
8364
  quoteGroup = prefixNameQuoteGroups.get(part);
6569
8365
  const quotedPresence = part.quoted && !((arrayOwned || prefixOwned) && isQuoteMarker(part));
8366
+ const selector = getArraySelector(part);
6570
8367
  if (part.kind === "variable" && ["-", "+", ":-", ":+"].includes(part.operator ?? "") && /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name)) {
6571
- const value = this.variable(state, part.name);
8368
+ let value = this.variable(state, part.name);
8369
+ if (selector?.kind === "element") {
8370
+ const index = numericIndex(selector.index, 4294967295);
8371
+ if (index === undefined)
8372
+ throw new ArrayFailure("index outside 0..4294967295");
8373
+ const binding = arrayStore(state)?.get(part.name);
8374
+ value = binding ? binding.getValue(index) : index === 0 ? value : undefined;
8375
+ }
6572
8376
  const missing = value === undefined || (part.operator.startsWith(":") && value === "");
6573
8377
  if (part.operator.endsWith("+") ? !missing : missing) {
6574
8378
  const operandIO = this.parameterOperandIO(part.alternate, state, partIO);
@@ -6580,7 +8384,6 @@ export class Runtime {
6580
8384
  continue;
6581
8385
  }
6582
8386
  }
6583
- const selector = getArraySelector(part);
6584
8387
  if (part.kind === "variable" && part.prefixNames === "@" && split && (part.quoted || state.variables.IFS === "")) {
6585
8388
  let position = 0;
6586
8389
  for await (const name of this.prefixNames(part.name, state)) {
@@ -6594,8 +8397,8 @@ export class Runtime {
6594
8397
  emptyNameGroups.add(quoteGroup);
6595
8398
  }
6596
8399
  }
6597
- else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
6598
- const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
8400
+ else if (part.kind === "variable" && split && (selector && selector.kind !== "element" && !part.length && (selector.kind === "members" ? !part.quoted || selector.separator === "@" : selector.separator === "@" && (part.quoted || state.variables.IFS === "")) || part.transform && part.name === "@")) {
8401
+ const members = selector && selector.kind !== "element" ? await this.arrayMembers(part.name, state, io, selector.kind === "keys" || part.keys === true) : this.positionalValues(state);
6599
8402
  for (let position = 0; position < members.length; position++) {
6600
8403
  if (position > 0)
6601
8404
  addField();
@@ -6628,7 +8431,7 @@ export class Runtime {
6628
8431
  fields[0].present = false;
6629
8432
  }
6630
8433
  else {
6631
- const value = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, partIO, hereString);
8434
+ const value = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, partIO, hereString, split, hereDocument);
6632
8435
  if (part.quoted || !split || state.variables.IFS === "")
6633
8436
  append(value, !part.quoted, quotedPresence || !split || shellValueByteLength(value) > 0);
6634
8437
  else
@@ -6749,6 +8552,62 @@ export class Runtime {
6749
8552
  if (start < bytes.length)
6750
8553
  yield start === 0 ? value : shellValueFromBytes(bytes.subarray(start), io[valueScope]);
6751
8554
  }
8555
+ async *splitRawValue(value, separators, io, byteCount) {
8556
+ if (typeof value === "string" && typeof separators === "string" && !byteCount) {
8557
+ for (const character of value)
8558
+ yield { value: character, separator: separators.includes(character), whitespace: " \t\n".includes(character) };
8559
+ return;
8560
+ }
8561
+ const work = this.splitWork ??= { scanned: 0 };
8562
+ const bytes = shellValueBytes(value, io[valueScope]);
8563
+ const delimiters = shellValueBytes(separators, io[valueScope]);
8564
+ const key = (input, offset, length) => {
8565
+ let result = 1;
8566
+ for (let index = offset; index < offset + length; index++)
8567
+ result = result * 257 + input[index];
8568
+ return result;
8569
+ };
8570
+ const keys = new Set();
8571
+ for (let offset = 0; offset < delimiters.length;) {
8572
+ const length = shellCharacterWidth(delimiters, offset, byteCount);
8573
+ const token = key(delimiters, offset, length);
8574
+ if (!keys.has(token)) {
8575
+ io[valueScope]?.reserve(32, 1);
8576
+ keys.add(token);
8577
+ }
8578
+ for (let index = offset; index < offset + length; index++) {
8579
+ const byteKey = key(delimiters, index, 1);
8580
+ if (!keys.has(byteKey)) {
8581
+ io[valueScope]?.reserve(32, 1);
8582
+ keys.add(byteKey);
8583
+ }
8584
+ }
8585
+ offset += length;
8586
+ work.scanned += length;
8587
+ if (work.scanned >= 4096) {
8588
+ work.scanned = 0;
8589
+ await yieldTurn(this.signal);
8590
+ }
8591
+ }
8592
+ let start = 0;
8593
+ for (let offset = 0; offset < bytes.length;) {
8594
+ const length = shellCharacterWidth(bytes, offset, byteCount);
8595
+ if (keys.has(key(bytes, offset, length))) {
8596
+ if (start < offset)
8597
+ yield { value: shellValueFromBytes(bytes.subarray(start, offset), io[valueScope]), separator: false, whitespace: false };
8598
+ yield { value: "", separator: true, whitespace: length === 1 && (bytes[offset] === 32 || bytes[offset] === 9 || bytes[offset] === 10) };
8599
+ start = offset + length;
8600
+ }
8601
+ offset += length;
8602
+ work.scanned += length;
8603
+ if (work.scanned >= 4096) {
8604
+ work.scanned = 0;
8605
+ await yieldTurn(this.signal);
8606
+ }
8607
+ }
8608
+ if (start < bytes.length)
8609
+ yield { value: start === 0 ? value : shellValueFromBytes(bytes.subarray(start), io[valueScope]), separator: false, whitespace: false };
8610
+ }
6752
8611
  positionalValues(state) {
6753
8612
  return state.positional.map((text, index) => stateMonitor(state)?.positionals.get(String(index), text) ?? text);
6754
8613
  }
@@ -6782,7 +8641,8 @@ export class Runtime {
6782
8641
  const binding = store.get(name);
6783
8642
  store.owner.reserve({ metadata: 64, work: 3 });
6784
8643
  if (!binding) {
6785
- const value = state.variables[name];
8644
+ const text = state.variables[name];
8645
+ const value = text === undefined ? undefined : keys ? "0" : stateMonitor(state)?.values.get(name, text) ?? text;
6786
8646
  if (value === undefined)
6787
8647
  return [];
6788
8648
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
@@ -6795,10 +8655,8 @@ export class Runtime {
6795
8655
  const values = [];
6796
8656
  for (const index of indices) {
6797
8657
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 4 });
6798
- const token = keys ? binding.associative ? binding.keys.get(binding.keyByIndex.get(index)).text : await textToken(store.owner, String(index), this.signal) : binding.values.get(index).text;
6799
- const value = token.rawValue === undefined ? token.value : shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
6800
- if (typeof value === "string")
6801
- await textToken(store.owner, value, this.signal);
8658
+ const value = keys ? binding.associative ? binding.keys.get(binding.keyByIndex.get(index)).text.shellValue : String(index) : binding.getValue(index);
8659
+ await textToken(store.owner, value, this.signal);
6802
8660
  values.push(value);
6803
8661
  await store.owner.ledger.checkpoint(this.signal);
6804
8662
  }