@fnndsc/brasa 0.56.1 → 0.56.3

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 (361) hide show
  1. package/dist/buildinfo.json +1 -1
  2. package/dist/builtins/berth/berth.d.ts +90 -3
  3. package/dist/builtins/berth/berth.js +129 -76
  4. package/dist/builtins/berth/berth.js.map +1 -1
  5. package/dist/builtins/berth/berthClose.d.ts +25 -6
  6. package/dist/builtins/berth/berthClose.js +8 -64
  7. package/dist/builtins/berth/berthClose.js.map +1 -1
  8. package/dist/builtins/berth/berthCube.d.ts +3 -2
  9. package/dist/builtins/berth/berthCube.js +9 -16
  10. package/dist/builtins/berth/berthCube.js.map +1 -1
  11. package/dist/builtins/berth/berthProcesses.js +33 -3
  12. package/dist/builtins/berth/berthProcesses.js.map +1 -1
  13. package/dist/builtins/debug.d.ts +24 -3
  14. package/dist/builtins/debug.js +66 -13
  15. package/dist/builtins/debug.js.map +1 -1
  16. package/dist/builtins/fs/acl.args.d.ts +5 -38
  17. package/dist/builtins/fs/acl.args.js +6 -72
  18. package/dist/builtins/fs/acl.args.js.map +1 -1
  19. package/dist/builtins/fs/acl.d.ts +100 -10
  20. package/dist/builtins/fs/acl.js +222 -68
  21. package/dist/builtins/fs/acl.js.map +1 -1
  22. package/dist/builtins/fs/cat.args.d.ts +14 -15
  23. package/dist/builtins/fs/cat.args.js +13 -40
  24. package/dist/builtins/fs/cat.args.js.map +1 -1
  25. package/dist/builtins/fs/cat.d.ts +40 -3
  26. package/dist/builtins/fs/cat.js +92 -13
  27. package/dist/builtins/fs/cat.js.map +1 -1
  28. package/dist/builtins/fs/cd.d.ts +24 -6
  29. package/dist/builtins/fs/cd.js +71 -18
  30. package/dist/builtins/fs/cd.js.map +1 -1
  31. package/dist/builtins/fs/configWrite.d.ts +24 -8
  32. package/dist/builtins/fs/configWrite.js +77 -36
  33. package/dist/builtins/fs/configWrite.js.map +1 -1
  34. package/dist/builtins/fs/cp.d.ts +39 -4
  35. package/dist/builtins/fs/cp.js +122 -56
  36. package/dist/builtins/fs/cp.js.map +1 -1
  37. package/dist/builtins/fs/download.d.ts +31 -4
  38. package/dist/builtins/fs/download.js +75 -38
  39. package/dist/builtins/fs/download.js.map +1 -1
  40. package/dist/builtins/fs/du.d.ts +59 -33
  41. package/dist/builtins/fs/du.js +111 -79
  42. package/dist/builtins/fs/du.js.map +1 -1
  43. package/dist/builtins/fs/edit.d.ts +19 -4
  44. package/dist/builtins/fs/edit.js +77 -16
  45. package/dist/builtins/fs/edit.js.map +1 -1
  46. package/dist/builtins/fs/find.d.ts +25 -0
  47. package/dist/builtins/fs/find.js +76 -5
  48. package/dist/builtins/fs/find.js.map +1 -1
  49. package/dist/builtins/fs/ls.d.ts +69 -4
  50. package/dist/builtins/fs/ls.js +84 -45
  51. package/dist/builtins/fs/ls.js.map +1 -1
  52. package/dist/builtins/fs/mkdir.d.ts +28 -18
  53. package/dist/builtins/fs/mkdir.js +95 -66
  54. package/dist/builtins/fs/mkdir.js.map +1 -1
  55. package/dist/builtins/fs/mv.d.ts +34 -4
  56. package/dist/builtins/fs/mv.js +129 -64
  57. package/dist/builtins/fs/mv.js.map +1 -1
  58. package/dist/builtins/fs/pull.args.d.ts +56 -17
  59. package/dist/builtins/fs/pull.args.js +16 -66
  60. package/dist/builtins/fs/pull.args.js.map +1 -1
  61. package/dist/builtins/fs/pull.d.ts +62 -0
  62. package/dist/builtins/fs/pull.js +123 -31
  63. package/dist/builtins/fs/pull.js.map +1 -1
  64. package/dist/builtins/fs/pwd.d.ts +19 -5
  65. package/dist/builtins/fs/pwd.js +62 -8
  66. package/dist/builtins/fs/pwd.js.map +1 -1
  67. package/dist/builtins/fs/rm.d.ts +58 -21
  68. package/dist/builtins/fs/rm.js +99 -82
  69. package/dist/builtins/fs/rm.js.map +1 -1
  70. package/dist/builtins/fs/rmdir.d.ts +29 -11
  71. package/dist/builtins/fs/rmdir.js +87 -36
  72. package/dist/builtins/fs/rmdir.js.map +1 -1
  73. package/dist/builtins/fs/touch.d.ts +44 -9
  74. package/dist/builtins/fs/touch.js +91 -27
  75. package/dist/builtins/fs/touch.js.map +1 -1
  76. package/dist/builtins/fs/tree.d.ts +36 -9
  77. package/dist/builtins/fs/tree.js +126 -36
  78. package/dist/builtins/fs/tree.js.map +1 -1
  79. package/dist/builtins/fs/upload.d.ts +22 -3
  80. package/dist/builtins/fs/upload.js +82 -14
  81. package/dist/builtins/fs/upload.js.map +1 -1
  82. package/dist/builtins/fs/xattr.args.d.ts +12 -33
  83. package/dist/builtins/fs/xattr.args.js +17 -95
  84. package/dist/builtins/fs/xattr.args.js.map +1 -1
  85. package/dist/builtins/fs/xattr.d.ts +98 -8
  86. package/dist/builtins/fs/xattr.js +163 -24
  87. package/dist/builtins/fs/xattr.js.map +1 -1
  88. package/dist/builtins/games/bytes.d.ts +201 -5
  89. package/dist/builtins/games/bytes.js +255 -54
  90. package/dist/builtins/games/bytes.js.map +1 -1
  91. package/dist/builtins/games/chimes.d.ts +180 -3
  92. package/dist/builtins/games/chimes.js +203 -26
  93. package/dist/builtins/games/chimes.js.map +1 -1
  94. package/dist/builtins/games/chrislab.d.ts +101 -3
  95. package/dist/builtins/games/chrislab.js +117 -17
  96. package/dist/builtins/games/chrislab.js.map +1 -1
  97. package/dist/builtins/games/cowsay.d.ts +93 -2
  98. package/dist/builtins/games/cowsay.js +90 -31
  99. package/dist/builtins/games/cowsay.js.map +1 -1
  100. package/dist/builtins/games/figlet.d.ts +70 -2
  101. package/dist/builtins/games/figlet.js +83 -6
  102. package/dist/builtins/games/figlet.js.map +1 -1
  103. package/dist/builtins/games/lab.d.ts +104 -5
  104. package/dist/builtins/games/lab.js +144 -6
  105. package/dist/builtins/games/lab.js.map +1 -1
  106. package/dist/builtins/games/lolcat.d.ts +61 -1
  107. package/dist/builtins/games/lolcat.js +59 -24
  108. package/dist/builtins/games/lolcat.js.map +1 -1
  109. package/dist/builtins/games/morse.d.ts +36 -1
  110. package/dist/builtins/games/morse.js +50 -2
  111. package/dist/builtins/games/morse.js.map +1 -1
  112. package/dist/builtins/games/numbers.d.ts +241 -5
  113. package/dist/builtins/games/numbers.js +298 -23
  114. package/dist/builtins/games/numbers.js.map +1 -1
  115. package/dist/builtins/games/play.d.ts +96 -6
  116. package/dist/builtins/games/play.js +137 -6
  117. package/dist/builtins/games/play.js.map +1 -1
  118. package/dist/builtins/games/qr.d.ts +40 -1
  119. package/dist/builtins/games/qr.js +51 -2
  120. package/dist/builtins/games/qr.js.map +1 -1
  121. package/dist/builtins/games/show.d.ts +131 -6
  122. package/dist/builtins/games/show.js +208 -20
  123. package/dist/builtins/games/show.js.map +1 -1
  124. package/dist/builtins/games/sky.d.ts +178 -4
  125. package/dist/builtins/games/sky.js +228 -12
  126. package/dist/builtins/games/sky.js.map +1 -1
  127. package/dist/builtins/games/text.d.ts +258 -7
  128. package/dist/builtins/games/text.js +310 -29
  129. package/dist/builtins/games/text.js.map +1 -1
  130. package/dist/builtins/games/trek.d.ts +67 -3
  131. package/dist/builtins/games/trek.js +63 -10
  132. package/dist/builtins/games/trek.js.map +1 -1
  133. package/dist/builtins/games/wtf.d.ts +58 -1
  134. package/dist/builtins/games/wtf.js +67 -6
  135. package/dist/builtins/games/wtf.js.map +1 -1
  136. package/dist/builtins/help.d.ts +41 -3
  137. package/dist/builtins/help.js +92 -796
  138. package/dist/builtins/help.js.map +1 -1
  139. package/dist/builtins/net/connect.d.ts +36 -3
  140. package/dist/builtins/net/connect.js +84 -26
  141. package/dist/builtins/net/connect.js.map +1 -1
  142. package/dist/builtins/net/cubepath.d.ts +49 -5
  143. package/dist/builtins/net/cubepath.js +69 -27
  144. package/dist/builtins/net/cubepath.js.map +1 -1
  145. package/dist/builtins/net/logout.d.ts +13 -4
  146. package/dist/builtins/net/logout.js +49 -3
  147. package/dist/builtins/net/logout.js.map +1 -1
  148. package/dist/builtins/net/netstat.d.ts +29 -4
  149. package/dist/builtins/net/netstat.js +64 -23
  150. package/dist/builtins/net/netstat.js.map +1 -1
  151. package/dist/builtins/net/pacs.d.ts +169 -15
  152. package/dist/builtins/net/pacs.js +92 -33
  153. package/dist/builtins/net/pacs.js.map +1 -1
  154. package/dist/builtins/net/query.args.d.ts +95 -0
  155. package/dist/builtins/net/query.args.js +26 -0
  156. package/dist/builtins/net/query.args.js.map +1 -0
  157. package/dist/builtins/net/query.d.ts +98 -5
  158. package/dist/builtins/net/query.js +89 -83
  159. package/dist/builtins/net/query.js.map +1 -1
  160. package/dist/builtins/net/status.d.ts +5 -3
  161. package/dist/builtins/net/status.js +5 -12
  162. package/dist/builtins/net/status.js.map +1 -1
  163. package/dist/builtins/parametersofplugin.d.ts +82 -3
  164. package/dist/builtins/parametersofplugin.js +79 -23
  165. package/dist/builtins/parametersofplugin.js.map +1 -1
  166. package/dist/builtins/proc.d.ts +210 -3
  167. package/dist/builtins/proc.js +210 -119
  168. package/dist/builtins/proc.js.map +1 -1
  169. package/dist/builtins/res/compute.d.ts +67 -3
  170. package/dist/builtins/res/compute.js +121 -35
  171. package/dist/builtins/res/compute.js.map +1 -1
  172. package/dist/builtins/res/dicom.d.ts +55 -7
  173. package/dist/builtins/res/dicom.js +86 -52
  174. package/dist/builtins/res/dicom.js.map +1 -1
  175. package/dist/builtins/res/expect.d.ts +52 -3
  176. package/dist/builtins/res/expect.js +92 -29
  177. package/dist/builtins/res/expect.js.map +1 -1
  178. package/dist/builtins/res/feed.d.ts +475 -3
  179. package/dist/builtins/res/feed.js +253 -158
  180. package/dist/builtins/res/feed.js.map +1 -1
  181. package/dist/builtins/res/files.d.ts +517 -3
  182. package/dist/builtins/res/files.js +124 -44
  183. package/dist/builtins/res/files.js.map +1 -1
  184. package/dist/builtins/res/gather.d.ts +113 -3
  185. package/dist/builtins/res/gather.js +153 -44
  186. package/dist/builtins/res/gather.js.map +1 -1
  187. package/dist/builtins/res/gather.process.d.ts +27 -19
  188. package/dist/builtins/res/gather.process.js +29 -59
  189. package/dist/builtins/res/gather.process.js.map +1 -1
  190. package/dist/builtins/res/group.d.ts +284 -3
  191. package/dist/builtins/res/group.js +147 -76
  192. package/dist/builtins/res/group.js.map +1 -1
  193. package/dist/builtins/res/image.d.ts +30 -2
  194. package/dist/builtins/res/image.js +62 -10
  195. package/dist/builtins/res/image.js.map +1 -1
  196. package/dist/builtins/res/play.d.ts +71 -9
  197. package/dist/builtins/res/play.js +124 -82
  198. package/dist/builtins/res/play.js.map +1 -1
  199. package/dist/builtins/res/plugininstance.d.ts +307 -2
  200. package/dist/builtins/res/plugininstance.js +122 -50
  201. package/dist/builtins/res/plugininstance.js.map +1 -1
  202. package/dist/builtins/res/pluginmeta.d.ts +244 -2
  203. package/dist/builtins/res/pluginmeta.js +98 -40
  204. package/dist/builtins/res/pluginmeta.js.map +1 -1
  205. package/dist/builtins/res/record.d.ts +37 -3
  206. package/dist/builtins/res/record.js +127 -57
  207. package/dist/builtins/res/record.js.map +1 -1
  208. package/dist/builtins/res/resourceList.d.ts +136 -0
  209. package/dist/builtins/res/resourceList.js +51 -0
  210. package/dist/builtins/res/resourceList.js.map +1 -0
  211. package/dist/builtins/res/tag.d.ts +244 -3
  212. package/dist/builtins/res/tag.js +97 -41
  213. package/dist/builtins/res/tag.js.map +1 -1
  214. package/dist/builtins/res/user.d.ts +122 -3
  215. package/dist/builtins/res/user.js +83 -21
  216. package/dist/builtins/res/user.js.map +1 -1
  217. package/dist/builtins/res/workflow.d.ts +244 -2
  218. package/dist/builtins/res/workflow.js +97 -40
  219. package/dist/builtins/res/workflow.js.map +1 -1
  220. package/dist/builtins/store.d.ts +85 -3
  221. package/dist/builtins/store.js +135 -68
  222. package/dist/builtins/store.js.map +1 -1
  223. package/dist/builtins/sys/apropos.d.ts +80 -5
  224. package/dist/builtins/sys/apropos.js +121 -15
  225. package/dist/builtins/sys/apropos.js.map +1 -1
  226. package/dist/builtins/sys/cal.d.ts +44 -5
  227. package/dist/builtins/sys/cal.js +57 -10
  228. package/dist/builtins/sys/cal.js.map +1 -1
  229. package/dist/builtins/sys/command.d.ts +20 -0
  230. package/dist/builtins/sys/command.js +31 -0
  231. package/dist/builtins/sys/command.js.map +1 -1
  232. package/dist/builtins/sys/context.d.ts +13 -3
  233. package/dist/builtins/sys/context.js +51 -4
  234. package/dist/builtins/sys/context.js.map +1 -1
  235. package/dist/builtins/sys/date.d.ts +56 -5
  236. package/dist/builtins/sys/date.js +63 -9
  237. package/dist/builtins/sys/date.js.map +1 -1
  238. package/dist/builtins/sys/echo.d.ts +63 -2
  239. package/dist/builtins/sys/echo.js +87 -8
  240. package/dist/builtins/sys/echo.js.map +1 -1
  241. package/dist/builtins/sys/fortune.d.ts +27 -2
  242. package/dist/builtins/sys/fortune.js +44 -2
  243. package/dist/builtins/sys/fortune.js.map +1 -1
  244. package/dist/builtins/sys/grep.d.ts +245 -11
  245. package/dist/builtins/sys/grep.js +418 -131
  246. package/dist/builtins/sys/grep.js.map +1 -1
  247. package/dist/builtins/sys/joblets.d.ts +66 -7
  248. package/dist/builtins/sys/joblets.js +101 -14
  249. package/dist/builtins/sys/joblets.js.map +1 -1
  250. package/dist/builtins/sys/motd.d.ts +37 -3
  251. package/dist/builtins/sys/motd.js +49 -8
  252. package/dist/builtins/sys/motd.js.map +1 -1
  253. package/dist/builtins/sys/notes.d.ts +75 -3
  254. package/dist/builtins/sys/notes.js +90 -29
  255. package/dist/builtins/sys/notes.js.map +1 -1
  256. package/dist/builtins/sys/physicalmode.d.ts +24 -3
  257. package/dist/builtins/sys/physicalmode.js +68 -14
  258. package/dist/builtins/sys/physicalmode.js.map +1 -1
  259. package/dist/builtins/sys/tasks.d.ts +74 -5
  260. package/dist/builtins/sys/tasks.js +102 -14
  261. package/dist/builtins/sys/tasks.js.map +1 -1
  262. package/dist/builtins/sys/textFilters.d.ts +673 -41
  263. package/dist/builtins/sys/textFilters.js +719 -264
  264. package/dist/builtins/sys/textFilters.js.map +1 -1
  265. package/dist/builtins/sys/timing.d.ts +24 -3
  266. package/dist/builtins/sys/timing.js +67 -14
  267. package/dist/builtins/sys/timing.js.map +1 -1
  268. package/dist/builtins/sys/variables.d.ts +70 -4
  269. package/dist/builtins/sys/variables.js +103 -31
  270. package/dist/builtins/sys/variables.js.map +1 -1
  271. package/dist/builtins/sys/version.d.ts +27 -2
  272. package/dist/builtins/sys/version.js +48 -3
  273. package/dist/builtins/sys/version.js.map +1 -1
  274. package/dist/builtins/sys/weather.d.ts +92 -15
  275. package/dist/builtins/sys/weather.js +71 -45
  276. package/dist/builtins/sys/weather.js.map +1 -1
  277. package/dist/builtins/sys/whoami.d.ts +38 -18
  278. package/dist/builtins/sys/whoami.js +112 -8
  279. package/dist/builtins/sys/whoami.js.map +1 -1
  280. package/dist/builtins/utils.d.ts +1 -38
  281. package/dist/builtins/utils.js +1 -87
  282. package/dist/builtins/utils.js.map +1 -1
  283. package/dist/chris/backend.js +2 -0
  284. package/dist/chris/backend.js.map +1 -1
  285. package/dist/chris/chrisCommands.js +62 -159
  286. package/dist/chris/chrisCommands.js.map +1 -1
  287. package/dist/chris/commandFallback.d.ts +12 -0
  288. package/dist/chris/commandFallback.js +47 -0
  289. package/dist/chris/commandFallback.js.map +1 -1
  290. package/dist/chris/files.js +24 -85
  291. package/dist/chris/files.js.map +1 -1
  292. package/dist/chris/filesystem.js +7 -5
  293. package/dist/chris/filesystem.js.map +1 -1
  294. package/dist/chris/help.js +0 -1082
  295. package/dist/chris/help.js.map +1 -1
  296. package/dist/chris/kinds.d.ts +30 -0
  297. package/dist/chris/kinds.js +154 -0
  298. package/dist/chris/kinds.js.map +1 -0
  299. package/dist/chris/mounts.d.ts +4 -4
  300. package/dist/chris/mounts.js +9 -16
  301. package/dist/chris/mounts.js.map +1 -1
  302. package/dist/chris/navigation.d.ts +2 -3
  303. package/dist/chris/navigation.js +6 -20
  304. package/dist/chris/navigation.js.map +1 -1
  305. package/dist/chris/pacsResources.d.ts +259 -0
  306. package/dist/chris/pacsResources.js +168 -0
  307. package/dist/chris/pacsResources.js.map +1 -0
  308. package/dist/chris/promptContext.d.ts +6 -0
  309. package/dist/chris/promptContext.js +3 -1
  310. package/dist/chris/promptContext.js.map +1 -1
  311. package/dist/core/argumentKinds.d.ts +107 -0
  312. package/dist/core/argumentKinds.js +223 -0
  313. package/dist/core/argumentKinds.js.map +1 -0
  314. package/dist/core/backend.d.ts +4 -1
  315. package/dist/core/backend.js.map +1 -1
  316. package/dist/core/commandDeclaration.d.ts +453 -0
  317. package/dist/core/commandDeclaration.js +52 -0
  318. package/dist/core/commandDeclaration.js.map +1 -0
  319. package/dist/core/commandParse.d.ts +64 -0
  320. package/dist/core/commandParse.js +468 -0
  321. package/dist/core/commandParse.js.map +1 -0
  322. package/dist/core/commandRegistry.d.ts +37 -0
  323. package/dist/core/commandRegistry.js +100 -3
  324. package/dist/core/commandRegistry.js.map +1 -1
  325. package/dist/core/commandSchema.d.ts +32 -0
  326. package/dist/core/commandSchema.js +117 -0
  327. package/dist/core/commandSchema.js.map +1 -0
  328. package/dist/core/commandUsage.d.ts +62 -0
  329. package/dist/core/commandUsage.js +247 -0
  330. package/dist/core/commandUsage.js.map +1 -0
  331. package/dist/core/coreCommands.js +66 -214
  332. package/dist/core/coreCommands.js.map +1 -1
  333. package/dist/core/dispatch.js +15 -8
  334. package/dist/core/dispatch.js.map +1 -1
  335. package/dist/core/paths.d.ts +51 -0
  336. package/dist/core/paths.js +86 -2
  337. package/dist/core/paths.js.map +1 -1
  338. package/dist/core/preprocess.d.ts +10 -9
  339. package/dist/core/preprocess.js +10 -20
  340. package/dist/core/preprocess.js.map +1 -1
  341. package/dist/core/warmupFailures.d.ts +34 -2
  342. package/dist/core/warmupFailures.js +43 -2
  343. package/dist/core/warmupFailures.js.map +1 -1
  344. package/dist/lib/completer/cfsPaths.d.ts +14 -0
  345. package/dist/lib/completer/cfsPaths.js +91 -0
  346. package/dist/lib/completer/cfsPaths.js.map +1 -0
  347. package/dist/lib/completer/declared.d.ts +15 -0
  348. package/dist/lib/completer/declared.js +189 -0
  349. package/dist/lib/completer/declared.js.map +1 -0
  350. package/dist/lib/completer/index.js +33 -79
  351. package/dist/lib/completer/index.js.map +1 -1
  352. package/dist/lib/parser.d.ts +5 -8
  353. package/dist/lib/parser.js +2 -11
  354. package/dist/lib/parser.js.map +1 -1
  355. package/dist/lib/vfs/vfs.d.ts +2 -1
  356. package/dist/lib/vfs/vfs.js +9 -6
  357. package/dist/lib/vfs/vfs.js.map +1 -1
  358. package/dist/lib/wildcard.js +2 -3
  359. package/dist/lib/wildcard.js.map +1 -1
  360. package/dist/notes.json +1 -1
  361. package/package.json +6 -5
@@ -7,11 +7,26 @@
7
7
  * runs none, so `find /proc/jobs | wc -l` had nowhere to go. As builtins
8
8
  * these run on every surface, and none starts a process anywhere.
9
9
  *
10
- * Each reads what was piped in, else the files named (`-` is the
11
- * pipe), and takes the POSIX flags most lines use, with their meanings. A
12
- * flag it does not take is the system program's: the line goes there with
13
- * the piped text when the surface can run one, and is refused by name when
14
- * it cannot. `command wc` skips the builtin altogether.
10
+ * Each reads what was piped in, else the files named (`-` is the pipe), and
11
+ * takes the POSIX flags most lines use, with their meanings. Each is declared
12
+ * in one record, so a flag it does not take is refused by the parser before
13
+ * it runs; `command wc` skips the builtin and runs the system program.
14
+ *
15
+ * Key components:
16
+ * - wcCommand, headCommand, tailCommand, sortCommand, uniqCommand, cutCommand: the declarations.
17
+ * - builtin_wc … builtin_cut: each declaration run over words already split.
18
+ * - filter_run: reads the sources and answers them, for any filter; filterFile_read,
19
+ * filterSources_read and filterRead_answer are its parts, which grep's files use too.
20
+ * - wc_render, head_lines, tail_lines, sort_lines, uniq_lines, cut_lines: the typed cores.
21
+ *
22
+ * Typical usage:
23
+ * ```ts
24
+ * await builtin_wc(['-l', 'notes.txt']);
25
+ * await builtin_head(['-5']);
26
+ * ```
27
+ *
28
+ * Dependencies: the session's filesystem (through the one store-path
29
+ * resolver) and what was piped in.
15
30
  *
16
31
  * @module
17
32
  */
@@ -19,30 +34,105 @@ import chalk from 'chalk';
19
34
  import { envelope_ok, envelope_error } from '@fnndsc/menu';
20
35
  import { errno_words } from '@fnndsc/fond';
21
36
  import { stdin_take } from '../games/stdin.js';
22
- import { surface_get } from '../../core/surface.js';
23
37
  import { vfsDispatcher_get } from '../../core/filesystem.js';
24
- import { path_resolve } from '../utils.js';
38
+ import { storePath_resolve } from '../utils.js';
25
39
  import { exitCode_set } from '../../core/handlerRun.js';
26
- /** A whole number from a flag's argument, or null. */
40
+ import { command_declare } from '../../core/commandDeclaration.js';
41
+ import { declaredCommand_run } from '../../core/commandParse.js';
42
+ /**
43
+ * The file operand every filter but `uniq` takes.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const GRAMMAR = { operands: [FILES_OPERAND] } as const satisfies CommandGrammar;
48
+ * ```
49
+ */
50
+ export const FILES_OPERAND = {
51
+ name: 'files', kind: 'cfsPath', optional: true, repeat: true, glob: true,
52
+ summary: 'ChRIS files to read, in order (a wildcard matches paths; - is the pipe); the pipe when none is named',
53
+ };
54
+ /** What the posix pages say of each filter. */
55
+ const POSIX_GROUP = 'Text';
56
+ /**
57
+ * A whole number from a flag's argument, or null.
58
+ *
59
+ * @param word - The argument.
60
+ * @returns The number, or null when it is not digits.
61
+ */
27
62
  function count_parse(word) {
28
- if (word === undefined || !/^\d+$/.test(word))
29
- return null;
30
- return Number(word);
63
+ return /^\d+$/.test(word) ? Number(word) : null;
31
64
  }
32
- /** The lines of a text, without the last line's newline. */
33
- function lines_split(text) {
65
+ /**
66
+ * The lines of a text, without the last line's newline.
67
+ *
68
+ * @param text - The text.
69
+ * @returns Its lines; none for the empty text.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * lines_split('a\nb\n'); // ['a', 'b']
74
+ * ```
75
+ */
76
+ export function lines_split(text) {
34
77
  if (text === '')
35
78
  return [];
36
79
  return (text.endsWith('\n') ? text.slice(0, -1) : text).split('\n');
37
80
  }
38
- /** Lines written back as text, each ended by a newline. */
39
- function lines_join(lines) {
81
+ /**
82
+ * Lines written back as text, each ended by a newline.
83
+ *
84
+ * @param lines - The lines.
85
+ * @returns The text; empty for no lines.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * lines_join(['a', 'b']); // 'a\nb\n'
90
+ * ```
91
+ */
92
+ export function lines_join(lines) {
40
93
  return lines.length === 0 ? '' : `${lines.join('\n')}\n`;
41
94
  }
42
- /** A word as a POSIX shell reads it back. */
43
- function word_quote(word) {
95
+ /**
96
+ * A word as a POSIX shell reads it back.
97
+ *
98
+ * @param word - The word.
99
+ * @returns The word, quoted when the shell would read it otherwise.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * word_quote("it's"); // "'it'\\''s'"
104
+ * ```
105
+ */
106
+ export function word_quote(word) {
44
107
  return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
45
108
  }
109
+ /**
110
+ * A filter's refusal: said on the error channel, the line's status 1.
111
+ *
112
+ * @param words - What was wrong.
113
+ * @returns The envelope.
114
+ */
115
+ function filter_refuse(words) {
116
+ exitCode_set(1);
117
+ return envelope_error('', undefined, `${words}\n`);
118
+ }
119
+ /**
120
+ * Reads one ChRIS file through the one store-path resolver, under its name as typed.
121
+ *
122
+ * @param command - The filter's name, for what it says.
123
+ * @param operand - The path as typed.
124
+ * @returns The source, or the line that says why it could not be read.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * await filterFile_read('wc', 'notes.txt'); // { name: 'notes.txt', text: '…' }
129
+ * ```
130
+ */
131
+ export async function filterFile_read(command, operand) {
132
+ const target = await storePath_resolve(operand);
133
+ const read = await vfsDispatcher_get().read(target);
134
+ return read.ok ? { name: operand, text: read.value } : { error: `${command}: ${operand}: ${read.reason ?? errno_words(read.errno)}\n` };
135
+ }
46
136
  /**
47
137
  * Reads what a filter works on: the files named, in order (`-` is the
48
138
  * pipe), else what was piped in.
@@ -50,8 +140,13 @@ function word_quote(word) {
50
140
  * @param command - The filter's name, for what it says.
51
141
  * @param operands - The file operands.
52
142
  * @returns The sources and what could not be read, or a refusal when there is nothing at all.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * await filterSources_read('wc', ['a.txt', '-']);
147
+ * ```
53
148
  */
54
- async function sources_read(command, operands) {
149
+ export async function filterSources_read(command, operands) {
55
150
  const piped = stdin_take();
56
151
  if (operands.length === 0) {
57
152
  if (piped === null)
@@ -65,123 +160,98 @@ async function sources_read(command, operands) {
65
160
  sources.push({ name: '-', text: piped ?? '' });
66
161
  continue;
67
162
  }
68
- const target = await path_resolve(operand);
69
- const read = await vfsDispatcher_get().read(target);
70
- if (read.ok)
71
- sources.push({ name: operand, text: read.value });
163
+ const read = await filterFile_read(command, operand);
164
+ if ('error' in read)
165
+ errors += read.error;
72
166
  else
73
- errors += `${command}: ${operand}: ${read.reason ?? errno_words(read.errno)}\n`;
167
+ sources.push(read);
74
168
  }
75
169
  return { sources, errors };
76
170
  }
77
171
  /**
78
- * Runs a filter: a line it does not take goes to the system program, else
79
- * the sources are read and answered.
172
+ * A filter's answer over what it read: the output, with what could not be
173
+ * read on the error channel (exit 1).
80
174
  *
81
- * @param command - The filter's name.
82
- * @param args - The words after it.
83
- * @param parsed - What they ask.
175
+ * @param read - What was read.
84
176
  * @param answer - Turns the sources into the filter's output.
85
177
  * @returns The envelope.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * filterRead_answer(read, (sources: FilterSource[]): string => wc_render(ask, sources));
182
+ * ```
86
183
  */
87
- async function filter_run(command, args, parsed, answer) {
88
- if (parsed.refused !== null) {
89
- exitCode_set(1);
90
- return envelope_error('', undefined, `${parsed.refused}\n`);
91
- }
92
- if (parsed.foreign !== null)
93
- return foreign_run(command, args, parsed.foreign, parsed.operands);
94
- const read = await sources_read(command, parsed.operands);
95
- if ('refusal' in read) {
96
- exitCode_set(1);
97
- return envelope_error('', undefined, `${read.refusal}\n`);
98
- }
184
+ export function filterRead_answer(read, answer) {
99
185
  if (read.errors !== '')
100
186
  exitCode_set(1);
101
- const out = read.sources.length > 0 || read.errors === '' ? answer(parsed.ask, read.sources) : '';
187
+ const out = read.sources.length > 0 || read.errors === '' ? answer(read.sources) : '';
102
188
  return read.errors === '' ? envelope_ok(out) : { ...envelope_ok(out), status: out === '' ? 'error' : 'ok', renderedErr: chalk.red(read.errors) };
103
189
  }
104
190
  /**
105
- * A line with a flag the builtin does not take: the system program's, fed
106
- * the piped text, where the surface runs one; refused by name where not.
191
+ * Runs a filter: reads its sources and answers them; a file it cannot read
192
+ * is said by name, and the others are still answered.
107
193
  *
108
194
  * @param command - The filter's name.
109
- * @param args - The words after it.
110
- * @param flag - The flag not taken.
111
- * @param operands - The file operands (session paths, which a system program cannot read).
195
+ * @param files - The file operands (`-` is the pipe); none for the pipe alone.
196
+ * @param answer - Turns the sources into the filter's output.
197
+ * @returns The envelope: the output, with what could not be read on the error channel.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * await filter_run('wc', ['notes.txt'], (sources: FilterSource[]): string => wc_render(ask, sources));
202
+ * ```
203
+ */
204
+ export async function filter_run(command, files, answer) {
205
+ const read = await filterSources_read(command, files);
206
+ if ('refusal' in read)
207
+ return filter_refuse(read.refusal);
208
+ return filterRead_answer(read, answer);
209
+ }
210
+ /**
211
+ * Runs a filter once its options are read: a refusal is said, else the
212
+ * sources are answered.
213
+ *
214
+ * @param command - The filter's name.
215
+ * @param read - What its options ask, or why not.
216
+ * @param files - The file operands.
217
+ * @param answer - Turns what it asks and the sources into its output.
112
218
  * @returns The envelope.
113
219
  */
114
- async function foreign_run(command, args, flag, operands) {
115
- const surface = surface_get();
116
- const text = stdin_take();
117
- if (!surface.capabilities.pipeSegments || operands.length > 0 || text === null) {
118
- exitCode_set(1);
119
- const why = operands.length > 0 || text === null
120
- ? `the builtin does not take it, and a system ${command} cannot read the session's files`
121
- : `the builtin does not take it, and this surface runs no system ${command} (command ${command} asks for one)`;
122
- return envelope_error('', undefined, `${command}: ${flag}: ${why}\n`);
123
- }
124
- const out = await surface.pipeSegment(`${command} ${args.map(word_quote).join(' ')}`.trim(), Buffer.from(text, 'utf-8'));
125
- return envelope_ok(out.toString('utf-8'));
126
- }
127
- /**
128
- * Reads short flags (`-lw`), each taking an argument attached or next
129
- * (`-n5`, `-n 5`), and operands; `--` ends the flags.
130
- *
131
- * @param args - The words.
132
- * @param takes - The flags without an argument.
133
- * @param withArg - The flags that take one.
134
- * @param long - Long flags to their short letter.
135
- * @returns The flags met (in order, with any argument), the operands, and a flag not taken.
136
- */
137
- function flags_read(args, takes, withArg, long = {}) {
138
- const flags = [];
139
- const operands = [];
140
- let ended = false;
141
- for (let i = 0; i < args.length; i++) {
142
- const word = args[i];
143
- if (ended || word === '-' || !word.startsWith('-')) {
144
- operands.push(word);
145
- continue;
146
- }
147
- if (word === '--') {
148
- ended = true;
149
- continue;
150
- }
151
- if (word.startsWith('--')) {
152
- const [name, value] = (word.includes('=') ? [word.slice(0, word.indexOf('=')), word.slice(word.indexOf('=') + 1)] : [word, undefined]);
153
- const letter = long[name];
154
- if (letter === undefined)
155
- return { flags, operands, foreign: word, missing: null };
156
- if (withArg.includes(letter)) {
157
- const arg = value ?? args[++i];
158
- if (arg === undefined)
159
- return { flags, operands, foreign: null, missing: name };
160
- flags.push([letter, arg]);
161
- }
162
- else {
163
- flags.push([letter, undefined]);
164
- }
165
- continue;
166
- }
167
- for (let j = 1; j < word.length; j++) {
168
- const letter = word[j];
169
- if (withArg.includes(letter)) {
170
- const rest = word.slice(j + 1);
171
- const arg = rest !== '' ? rest : args[++i];
172
- if (arg === undefined)
173
- return { flags, operands, foreign: null, missing: `-${letter}` };
174
- flags.push([letter, arg]);
175
- break;
176
- }
177
- if (!takes.includes(letter))
178
- return { flags, operands, foreign: `-${letter}`, missing: null };
179
- flags.push([letter, undefined]);
180
- }
181
- }
182
- return { flags, operands, foreign: null, missing: null };
220
+ async function askedFilter_run(command, read, files, answer) {
221
+ if ('refused' in read)
222
+ return filter_refuse(read.refused);
223
+ return filter_run(command, files, (sources) => answer(read.ask, sources));
183
224
  }
184
- /** Counts a text as `wc` does: newlines, words between blanks, bytes, characters. */
225
+ /**
226
+ * Every line of every source, in order.
227
+ *
228
+ * @param sources - The sources.
229
+ * @returns Their lines.
230
+ */
231
+ function sourceLines_list(sources) {
232
+ return sources.flatMap((s) => lines_split(s.text));
233
+ }
234
+ /** What a `wc` line may hold. */
235
+ const WC_GRAMMAR = {
236
+ operands: [FILES_OPERAND],
237
+ options: {
238
+ lines: { short: 'l', long: 'lines', summary: 'Count lines' },
239
+ words: { short: 'w', long: 'words', summary: 'Count words' },
240
+ bytes: { short: 'c', long: 'bytes', summary: 'Count bytes' },
241
+ chars: { short: 'm', long: 'chars', summary: 'Count characters' },
242
+ },
243
+ };
244
+ /**
245
+ * Counts a text as `wc` does: newlines, words between blanks, bytes, characters.
246
+ *
247
+ * @param text - The text.
248
+ * @returns Its counts.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * wc_count('héllo\n'); // { lines: 1, words: 1, bytes: 7, chars: 6 }
253
+ * ```
254
+ */
185
255
  export function wc_count(text) {
186
256
  return {
187
257
  lines: (text.match(/\n/g) ?? []).length,
@@ -190,28 +260,18 @@ export function wc_count(text) {
190
260
  chars: [...text].length,
191
261
  };
192
262
  }
193
- /** Reads `wc`'s words. */
194
- export function wcArgs_parse(args) {
195
- const read = flags_read(args, 'lwcm', '', { '--lines': 'l', '--words': 'w', '--bytes': 'c', '--chars': 'm' });
196
- const ask = { lines: false, words: false, bytes: false, chars: false };
197
- for (const [flag] of read.flags) {
198
- if (flag === 'l')
199
- ask.lines = true;
200
- if (flag === 'w')
201
- ask.words = true;
202
- if (flag === 'c')
203
- ask.bytes = true;
204
- if (flag === 'm')
205
- ask.chars = true;
206
- }
207
- if (!ask.lines && !ask.words && !ask.bytes && !ask.chars) {
208
- ask.lines = true;
209
- ask.words = true;
210
- ask.bytes = true;
211
- }
212
- return { ask, operands: read.operands, foreign: read.foreign, refused: null };
213
- }
214
- /** `wc`'s answer: a row per source (and a total for more than one), counts in POSIX order. */
263
+ /**
264
+ * `wc`'s answer: a row per source (and a total for more than one), counts in POSIX order.
265
+ *
266
+ * @param ask - What to count.
267
+ * @param sources - The sources.
268
+ * @returns The rows.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * wc_render({ lines: true, words: false, bytes: false, chars: false }, [{ name: null, text: 'a\n' }]); // '1\n'
273
+ * ```
274
+ */
215
275
  export function wc_render(ask, sources) {
216
276
  const rows = sources.map((s) => ({ counts: wc_count(s.text), name: s.name }));
217
277
  if (rows.length > 1) {
@@ -228,92 +288,304 @@ export function wc_render(ask, sources) {
228
288
  const width = single ? 1 : Math.max(...rows.flatMap((r) => pick(r.counts).map((n) => String(n).length)));
229
289
  return lines_join(rows.map((r) => [...pick(r.counts).map((n) => String(n).padStart(width)), ...(r.name === null ? [] : [r.name])].join(' ')));
230
290
  }
231
- /** `wc [-lwcm] [FILE]...` */
291
+ /**
292
+ * `wc [-lwcm] [FILE]...`: the declaration.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * commands_register({ declarations: [wcCommand] });
297
+ * ```
298
+ */
299
+ export const wcCommand = command_declare({
300
+ name: 'wc',
301
+ grammar: WC_GRAMMAR,
302
+ help: {
303
+ summary: 'Count lines, words and bytes',
304
+ description: 'Counts what is piped in, else each file named (- is the pipe), with a total for more than one. With no flag it counts lines, words and bytes; one count of the pipe prints the number alone. It runs in the session, on every surface; a flag it does not take is refused, and command wc runs the system wc.',
305
+ category: 'Files',
306
+ posix: { group: POSIX_GROUP, differs: 'as wc; a FILE is a ChRIS path' },
307
+ examples: ['find /proc/jobs -plugin pl-dylld | wc -l', 'wc -l notes.txt', 'cat a.csv | wc'],
308
+ },
309
+ holds: [],
310
+ handler: async (args) => {
311
+ const { lines, words, bytes, chars } = args.options;
312
+ const none = !lines && !words && !bytes && !chars;
313
+ const ask = none ? { lines: true, words: true, bytes: true, chars: false } : { lines, words, bytes, chars };
314
+ return filter_run('wc', args.operands.files, (sources) => wc_render(ask, sources));
315
+ },
316
+ });
317
+ /**
318
+ * `wc` over words already split: the declaration's parse and handler.
319
+ *
320
+ * @param args - The words after `wc`.
321
+ * @returns The counts, or the refusal of a bad line.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * await builtin_wc(['-l', 'notes.txt']);
326
+ * ```
327
+ */
232
328
  export async function builtin_wc(args) {
233
- return filter_run('wc', args, wcArgs_parse(args), wc_render);
234
- }
235
- /** Reads `head`'s or `tail`'s words: `-n N`, `-N`, and for head `-n -N`, for tail `-n +N`. */
236
- export function endsArgs_parse(command, args) {
237
- const ask = { count: 10, butLast: false, fromLine: false };
238
- // `-5` is `-n 5`, as both have always taken it.
239
- const words = args.map((w, i) => (/^-\d+$/.test(w) && args[i - 1] !== '-n' && args[i - 1] !== '--lines' ? `-n${w.slice(1)}` : w));
240
- const read = flags_read(words, '', 'n', { '--lines': 'n' });
241
- if (read.missing !== null)
242
- return { ask, operands: read.operands, foreign: null, refused: `${command}: ${read.missing} needs a number` };
243
- for (const [, value] of read.flags) {
244
- const raw = value ?? '';
245
- const sign = raw.startsWith('-') || raw.startsWith('+') ? raw[0] : '';
246
- const n = count_parse(sign === '' ? raw : raw.slice(1));
247
- const signTaken = sign === '' || (command === 'head' && sign === '-') || (command === 'tail' && sign === '+');
248
- if (n === null || !signTaken)
249
- return { ask, operands: read.operands, foreign: null, refused: `${command}: -n takes a number${command === 'head' ? ' (-N: all but the last N)' : ' (+N: from line N)'}, not '${raw}'` };
250
- ask.count = n;
251
- ask.butLast = command === 'head' && sign === '-';
252
- ask.fromLine = command === 'tail' && sign === '+';
329
+ return declaredCommand_run(wcCommand, args);
330
+ }
331
+ /** What a `head` line may hold. */
332
+ const HEAD_GRAMMAR = {
333
+ operands: [FILES_OPERAND],
334
+ options: {
335
+ lines: {
336
+ short: 'n', long: 'lines', numeral: true, value: { name: 'count', kind: 'word' },
337
+ summary: 'The first N lines (-N says the same); -n -N: all but the last N',
338
+ },
339
+ bytes: { short: 'c', long: 'bytes', value: { name: 'count', kind: 'word' }, summary: 'The first N bytes; -c -N: all but the last N' },
340
+ },
341
+ };
342
+ /** What a `tail` line may hold. */
343
+ const TAIL_GRAMMAR = {
344
+ operands: [FILES_OPERAND],
345
+ options: {
346
+ lines: {
347
+ short: 'n', long: 'lines', numeral: true, value: { name: 'count', kind: 'word' },
348
+ summary: 'The last N lines (-N says the same); -n +N: from line N to the end',
349
+ },
350
+ bytes: { short: 'c', long: 'bytes', value: { name: 'count', kind: 'word' }, summary: 'The last N bytes; -c +N: from byte N to the end' },
351
+ follow: { short: 'f', long: 'follow', summary: 'Taken: the text is read once and its last lines shown; a ChRIS file or a pipe is not followed' },
352
+ },
353
+ };
354
+ /** What tail says of -f, which it takes but cannot do. */
355
+ const FOLLOW_NOTE = 'tail: -f: a ChRIS file or a pipe is not followed; its last lines are shown once';
356
+ /**
357
+ * Reads `head`'s or `tail`'s count: N, and for head -N, for tail +N, of
358
+ * lines (-n) or bytes (-c).
359
+ *
360
+ * @param command - Which of the two.
361
+ * @param lines - The -n count as given, or undefined.
362
+ * @param bytes - The -c count as given, or undefined.
363
+ * @returns What it asks (ten lines when neither is given), or the refusal of a count that is not one.
364
+ */
365
+ function endsAsk_read(command, lines, bytes) {
366
+ if (lines !== undefined && bytes !== undefined)
367
+ return { refused: `${command}: -n and -c do not mix; take one` };
368
+ const raw = bytes ?? lines;
369
+ const flag = bytes !== undefined ? '-c' : '-n';
370
+ if (raw === undefined)
371
+ return { ask: { count: 10, butLast: false, fromLine: false, bytes: false } };
372
+ const sign = raw.startsWith('-') || raw.startsWith('+') ? raw[0] : '';
373
+ const n = count_parse(sign === '' ? raw : raw.slice(1));
374
+ const signTaken = sign === '' || (command === 'head' && sign === '-') || (command === 'tail' && sign === '+');
375
+ if (n === null || !signTaken) {
376
+ const unit = bytes !== undefined ? 'byte' : 'line';
377
+ return { refused: `${command}: ${flag} takes a number${command === 'head' ? ' (-N: all but the last N)' : ` (+N: from ${unit} N)`}, not '${raw}'` };
253
378
  }
254
- return { ask, operands: read.operands, foreign: read.foreign, refused: null };
379
+ return { ask: { count: n, butLast: command === 'head' && sign === '-', fromLine: command === 'tail' && sign === '+', bytes: bytes !== undefined } };
255
380
  }
256
- /** The lines `head` keeps of a text. */
381
+ /**
382
+ * The lines `head` keeps of a text.
383
+ *
384
+ * @param ask - How many.
385
+ * @param text - The text.
386
+ * @returns The kept lines.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * head_lines({ count: 1, butLast: false, fromLine: false, bytes: false }, 'a\nb\n'); // ['a']
391
+ * ```
392
+ */
257
393
  export function head_lines(ask, text) {
258
394
  const lines = lines_split(text);
259
395
  return ask.butLast ? lines.slice(0, Math.max(0, lines.length - ask.count)) : lines.slice(0, ask.count);
260
396
  }
261
- /** The lines `tail` keeps of a text. */
397
+ /**
398
+ * The lines `tail` keeps of a text.
399
+ *
400
+ * @param ask - How many.
401
+ * @param text - The text.
402
+ * @returns The kept lines.
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * tail_lines({ count: 1, butLast: false, fromLine: false, bytes: false }, 'a\nb\n'); // ['b']
407
+ * ```
408
+ */
262
409
  export function tail_lines(ask, text) {
263
410
  const lines = lines_split(text);
264
411
  if (ask.fromLine)
265
412
  return lines.slice(Math.max(0, ask.count - 1));
266
413
  return ask.count === 0 ? [] : lines.slice(-ask.count);
267
414
  }
268
- /** Each source's kept lines, under `==> name <==` when there is more than one. */
269
- function ends_render(keep) {
415
+ /**
416
+ * The bytes `head -c` keeps of a text, as text.
417
+ *
418
+ * @param ask - How many.
419
+ * @param text - The text.
420
+ * @returns The kept bytes.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * head_bytes({ count: 3, butLast: false, fromLine: false, bytes: true }, 'hello\n'); // 'hel'
425
+ * ```
426
+ */
427
+ export function head_bytes(ask, text) {
428
+ const all = Buffer.from(text, 'utf-8');
429
+ return (ask.butLast ? all.subarray(0, Math.max(0, all.length - ask.count)) : all.subarray(0, ask.count)).toString('utf-8');
430
+ }
431
+ /**
432
+ * The bytes `tail -c` keeps of a text, as text.
433
+ *
434
+ * @param ask - How many.
435
+ * @param text - The text.
436
+ * @returns The kept bytes.
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * tail_bytes({ count: 3, butLast: false, fromLine: false, bytes: true }, 'hello\n'); // 'lo\n'
441
+ * ```
442
+ */
443
+ export function tail_bytes(ask, text) {
444
+ const all = Buffer.from(text, 'utf-8');
445
+ if (ask.fromLine)
446
+ return all.subarray(Math.max(0, ask.count - 1)).toString('utf-8');
447
+ return (ask.count === 0 ? all.subarray(all.length) : all.subarray(-ask.count)).toString('utf-8');
448
+ }
449
+ /**
450
+ * Each source's kept part, under `==> name <==` when there is more than one.
451
+ *
452
+ * @param keepLines - The lines one source keeps.
453
+ * @param keepBytes - The bytes one source keeps.
454
+ * @returns The answer over the sources.
455
+ */
456
+ function ends_render(keepLines, keepBytes) {
457
+ const keep = (ask, text) => (ask.bytes ? keepBytes(ask, text) : lines_join(keepLines(ask, text)));
270
458
  return (ask, sources) => {
271
459
  if (sources.length === 1)
272
- return lines_join(keep(ask, sources[0].text));
273
- return sources.map((s) => `==> ${s.name === '-' ? 'standard input' : s.name} <==\n${lines_join(keep(ask, s.text))}`).join('\n');
460
+ return keep(ask, sources[0].text);
461
+ return sources.map((s) => `==> ${s.name === '-' ? 'standard input' : s.name ?? ''} <==\n${keep(ask, s.text)}`).join('\n');
274
462
  };
275
463
  }
276
- /** `head [-n N | -N] [FILE]...` */
464
+ /**
465
+ * `head [-n N | -N | -c N] [FILE]...`: the declaration.
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * commands_register({ declarations: [headCommand] });
470
+ * ```
471
+ */
472
+ export const headCommand = command_declare({
473
+ name: 'head',
474
+ grammar: HEAD_GRAMMAR,
475
+ help: {
476
+ summary: 'The first lines',
477
+ description: 'Prints the first ten lines of what is piped in, else of each file named (under ==> name <== for more than one). -N is -n N; -n -N prints all but the last N; -c N prints the first N bytes. It runs in the session, on every surface; a flag it does not take is refused, and command head runs the system head.',
478
+ category: 'Files',
479
+ posix: { group: POSIX_GROUP, differs: 'as head; a FILE is a ChRIS path' },
480
+ examples: ['ls -l | head -n 5', 'head -3 notes.txt', 'head -c 100 notes.txt'],
481
+ },
482
+ holds: [],
483
+ handler: async (args) => askedFilter_run('head', endsAsk_read('head', args.options.lines, args.options.bytes), args.operands.files, ends_render(head_lines, head_bytes)),
484
+ });
485
+ /**
486
+ * `tail [-n N | -N | -n +N | -c N] [-f] [FILE]...`: the declaration.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * commands_register({ declarations: [tailCommand] });
491
+ * ```
492
+ */
493
+ export const tailCommand = command_declare({
494
+ name: 'tail',
495
+ grammar: TAIL_GRAMMAR,
496
+ help: {
497
+ summary: 'The last lines',
498
+ description: 'Prints the last ten lines of what is piped in, else of each file named (under ==> name <== for more than one). -N is -n N; -n +N prints from line N on; -c N prints the last N bytes, -c +N from byte N on. -f is taken but cannot follow: a ChRIS file or a pipe is read once and its last lines shown, and tail says so. It runs in the session, on every surface; a flag it does not take is refused, and command tail runs the system tail.',
499
+ category: 'Files',
500
+ posix: { group: POSIX_GROUP, differs: 'as tail; -f shows the last lines once, never follows; a FILE is a ChRIS path' },
501
+ examples: ['cat /proc/jobs/feed_12/pl-dircopy_40/log | tail -n 20', 'tail -n +2 a.csv', 'tail -c 64 notes.txt'],
502
+ },
503
+ holds: [],
504
+ handler: async (args) => {
505
+ const envelope = await askedFilter_run('tail', endsAsk_read('tail', args.options.lines, args.options.bytes), args.operands.files, ends_render(tail_lines, tail_bytes));
506
+ if (!args.options.follow || envelope.status !== 'ok')
507
+ return envelope;
508
+ return { ...envelope, renderedErr: `${envelope.renderedErr ?? ''}${chalk.gray(FOLLOW_NOTE)}\n` };
509
+ },
510
+ });
511
+ /**
512
+ * `head` over words already split: the declaration's parse and handler.
513
+ *
514
+ * @param args - The words after `head`.
515
+ * @returns The lines, or the refusal of a bad line.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * await builtin_head(['-5', 'notes.txt']);
520
+ * ```
521
+ */
277
522
  export async function builtin_head(args) {
278
- return filter_run('head', args, endsArgs_parse('head', args), ends_render(head_lines));
523
+ return declaredCommand_run(headCommand, args);
279
524
  }
280
- /** `tail [-n N | -N | -n +N] [FILE]...` */
525
+ /**
526
+ * `tail` over words already split: the declaration's parse and handler.
527
+ *
528
+ * @param args - The words after `tail`.
529
+ * @returns The lines, or the refusal of a bad line.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * await builtin_tail(['-n', '+2', 'a.csv']);
534
+ * ```
535
+ */
281
536
  export async function builtin_tail(args) {
282
- return filter_run('tail', args, endsArgs_parse('tail', args), ends_render(tail_lines));
537
+ return declaredCommand_run(tailCommand, args);
283
538
  }
284
- /** Reads `sort`'s words. */
285
- export function sortArgs_parse(args) {
286
- const ask = { reverse: false, numeric: false, fold: false, unique: false, key: null, separator: null };
287
- const read = flags_read(args, 'rnfu', 'kt', {
288
- '--reverse': 'r', '--numeric-sort': 'n', '--ignore-case': 'f', '--unique': 'u', '--key': 'k', '--field-separator': 't',
289
- });
290
- if (read.missing !== null)
291
- return { ask, operands: read.operands, foreign: null, refused: `sort: ${read.missing} needs an argument` };
292
- for (const [flag, value] of read.flags) {
293
- if (flag === 'r')
294
- ask.reverse = true;
295
- if (flag === 'n')
296
- ask.numeric = true;
297
- if (flag === 'f')
298
- ask.fold = true;
299
- if (flag === 'u')
300
- ask.unique = true;
301
- if (flag === 't') {
302
- if ((value ?? '').length !== 1)
303
- return { ask, operands: read.operands, foreign: null, refused: `sort: -t takes one character, not '${value ?? ''}'` };
304
- ask.separator = value;
305
- }
306
- if (flag === 'k') {
307
- // A key's own options (-k2n, -k1.3) are the system sort's.
308
- const key = /^(\d+)(?:,(\d+))?$/.exec(value ?? '');
309
- if (key === null)
310
- return { ask, operands: read.operands, foreign: `-k ${value ?? ''}`, refused: null };
311
- ask.key = { from: Number(key[1]), to: key[2] === undefined ? null : Number(key[2]) };
539
+ /** What a `sort` line may hold. */
540
+ const SORT_GRAMMAR = {
541
+ operands: [FILES_OPERAND],
542
+ options: {
543
+ reverse: { short: 'r', long: 'reverse', summary: 'Reverse the order' },
544
+ numeric: { short: 'n', long: 'numeric-sort', summary: 'Compare by number' },
545
+ human: { short: 'h', long: 'human-numeric-sort', summary: 'Compare human sizes: 2K < 1M < 3G' },
546
+ version: { short: 'V', long: 'version-sort', summary: 'Compare versions: runs of digits as numbers (v2 < v10)' },
547
+ fold: { short: 'f', long: 'ignore-case', summary: 'Ignore case' },
548
+ unique: { short: 'u', long: 'unique', summary: 'One of each equal line' },
549
+ key: { short: 'k', long: 'key', value: { name: 'N[,M]', kind: 'word' }, summary: 'Compare fields N to M (to the end without M)' },
550
+ separator: { short: 't', long: 'field-separator', value: { name: 'C', kind: 'word' }, summary: 'Fields split at C, not at runs of blanks' },
551
+ },
552
+ };
553
+ /** The size suffixes `sort -h` reads, smallest first. */
554
+ const HUMAN_SUFFIXES = 'KMGTPEZY';
555
+ /**
556
+ * Reads `sort`'s options: its switches, a key of whole fields, a one-character separator.
557
+ *
558
+ * @param options - The typed options.
559
+ * @returns What they ask, or the refusal of a key, a separator or a mix of orders it does not take.
560
+ */
561
+ function sortAsk_read(options) {
562
+ const orders = [
563
+ ...(options.numeric ? ['numeric'] : []), ...(options.human ? ['human'] : []), ...(options.version ? ['version'] : []),
564
+ ];
565
+ if (orders.length > 1)
566
+ return { refused: 'sort: -n, -h and -V do not mix; take one' };
567
+ const ask = { reverse: options.reverse, compare: orders[0] ?? 'text', fold: options.fold, unique: options.unique, key: null, separator: null };
568
+ if (options.separator !== undefined) {
569
+ if (options.separator.length !== 1)
570
+ return { refused: `sort: -t takes one character, not '${options.separator}'` };
571
+ ask.separator = options.separator;
572
+ }
573
+ if (options.key !== undefined) {
574
+ const key = /^(\d+)(?:,(\d+))?$/.exec(options.key);
575
+ if (key === null) {
576
+ return { refused: `sort: -k takes whole fields, N or N,M, not '${options.key}'; a key with its own options is the system sort's: command sort …` };
312
577
  }
578
+ ask.key = { from: Number(key[1]), to: key[2] === undefined ? null : Number(key[2]) };
313
579
  }
314
- return { ask, operands: read.operands, foreign: read.foreign, refused: null };
580
+ return { ask };
315
581
  }
316
- /** The part of a line a sort compares. */
582
+ /**
583
+ * The part of a line a sort compares.
584
+ *
585
+ * @param ask - How it orders.
586
+ * @param line - The line.
587
+ * @returns The key's text.
588
+ */
317
589
  function sortKey_extract(ask, line) {
318
590
  if (ask.key === null)
319
591
  return line;
@@ -321,18 +593,68 @@ function sortKey_extract(ask, line) {
321
593
  const to = ask.key.to ?? fields.length;
322
594
  return fields.slice(ask.key.from - 1, to).join(ask.separator ?? ' ');
323
595
  }
324
- /** The lines in `sort`'s order: stable, by key, numeric or text, then reversed, then unique by key. */
596
+ /**
597
+ * Compares two keys by version: runs of digits as numbers, the rest as text.
598
+ *
599
+ * @param a - One key.
600
+ * @param b - The other.
601
+ * @returns Negative, zero or positive.
602
+ */
603
+ function version_compare(a, b) {
604
+ const runsA = a.match(/\d+|\D+/g) ?? [];
605
+ const runsB = b.match(/\d+|\D+/g) ?? [];
606
+ for (let i = 0; i < Math.min(runsA.length, runsB.length); i++) {
607
+ const x = runsA[i];
608
+ const y = runsB[i];
609
+ if (x === y)
610
+ continue;
611
+ const digits = /^\d/.test(x) && /^\d/.test(y);
612
+ if (digits) {
613
+ const delta = BigInt(x) - BigInt(y);
614
+ if (delta !== 0n)
615
+ return delta < 0n ? -1 : 1;
616
+ }
617
+ return x < y ? -1 : 1;
618
+ }
619
+ return runsA.length - runsB.length;
620
+ }
621
+ /**
622
+ * Compares two keys as the sort asks.
623
+ *
624
+ * @param ask - How it orders.
625
+ * @param a - One key.
626
+ * @param b - The other.
627
+ * @returns Negative, zero or positive.
628
+ */
629
+ function sortKeys_compare(ask, a, b) {
630
+ if (ask.compare === 'version')
631
+ return version_compare(a.key, b.key);
632
+ if (ask.compare === 'human' && a.rank !== b.rank)
633
+ return a.rank - b.rank;
634
+ if (ask.compare !== 'text' && a.num !== b.num)
635
+ return a.num - b.num;
636
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
637
+ }
638
+ /**
639
+ * The lines in `sort`'s order: stable, by key, as text, number, size or version, then reversed, then unique by key.
640
+ *
641
+ * @param ask - How it orders.
642
+ * @param lines - The lines.
643
+ * @returns The lines in order.
644
+ *
645
+ * @example
646
+ * ```ts
647
+ * sort_lines({ reverse: false, compare: 'human', fold: false, unique: false, key: null, separator: null }, ['1M', '2K']); // ['2K', '1M']
648
+ * ```
649
+ */
325
650
  export function sort_lines(ask, lines) {
326
651
  const keyed = lines.map((line) => {
327
652
  const key = sortKey_extract(ask, line);
328
- const num = Number(/^\s*-?\d+(?:\.\d+)?/.exec(key)?.[0] ?? 0);
329
- return { line, key: ask.fold ? key.toLowerCase() : key, num };
653
+ const number = /^\s*(-?\d+(?:\.\d+)?)([a-zA-Z]?)/.exec(key);
654
+ const rank = HUMAN_SUFFIXES.indexOf((number?.[2] ?? '').toUpperCase()) + 1;
655
+ return { line, key: ask.fold ? key.toLowerCase() : key, num: Number(number?.[1] ?? 0), rank: (number?.[2] ?? '') === '' ? 0 : rank };
330
656
  });
331
- const compare = (a, b) => {
332
- if (ask.numeric && a.num !== b.num)
333
- return a.num - b.num;
334
- return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
335
- };
657
+ const compare = (a, b) => sortKeys_compare(ask, a, b);
336
658
  keyed.sort((a, b) => (ask.reverse ? compare(b, a) : compare(a, b)));
337
659
  if (!ask.unique)
338
660
  return keyed.map((k) => k.line);
@@ -345,29 +667,63 @@ export function sort_lines(ask, lines) {
345
667
  }
346
668
  return out;
347
669
  }
348
- /** `sort [-rnfu] [-k N[,M]] [-t C] [FILE]...` */
670
+ /**
671
+ * `sort [-rnhVfu] [-k N[,M]] [-t C] [FILE]...`: the declaration.
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * commands_register({ declarations: [sortCommand] });
676
+ * ```
677
+ */
678
+ export const sortCommand = command_declare({
679
+ name: 'sort',
680
+ grammar: SORT_GRAMMAR,
681
+ help: {
682
+ summary: 'Lines in order',
683
+ description: 'Sorts the lines of what is piped in, else of the files named, stably: as text, or by number (-n), by human size (-h: 2K < 1M < 3G) or by version (-V: v2 < v10). -k compares fields N to M (to the end without M), split at -t C or at runs of blanks. It runs in the session, on every surface; a flag it does not take, or a key with its own options (-k2n), is refused, and command sort runs the system sort.',
684
+ category: 'Files',
685
+ posix: { group: POSIX_GROUP, differs: 'as sort, a key by whole fields; a FILE is a ChRIS path' },
686
+ examples: ['find /proc/jobs -status finishedWithError | sort', 'cat a.csv | sort -t , -k 2 -n', 'du -h | sort -h', 'ls | sort -V'],
687
+ },
688
+ holds: [],
689
+ handler: async (args) => askedFilter_run('sort', sortAsk_read(args.options), args.operands.files, (ask, sources) => lines_join(sort_lines(ask, sourceLines_list(sources)))),
690
+ });
691
+ /**
692
+ * `sort` over words already split: the declaration's parse and handler.
693
+ *
694
+ * @param args - The words after `sort`.
695
+ * @returns The lines in order, or the refusal of a bad line.
696
+ *
697
+ * @example
698
+ * ```ts
699
+ * await builtin_sort(['-t', ',', '-k', '2', 'a.csv']);
700
+ * ```
701
+ */
349
702
  export async function builtin_sort(args) {
350
- return filter_run('sort', args, sortArgs_parse(args), (ask, sources) => lines_join(sort_lines(ask, sources.flatMap((s) => lines_split(s.text)))));
351
- }
352
- /** Reads `uniq`'s words; one input at most, as uniq takes. */
353
- export function uniqArgs_parse(args) {
354
- const ask = { count: false, repeated: false, unique: false, fold: false };
355
- const read = flags_read(args, 'cdui', '', { '--count': 'c', '--repeated': 'd', '--unique': 'u', '--ignore-case': 'i' });
356
- for (const [flag] of read.flags) {
357
- if (flag === 'c')
358
- ask.count = true;
359
- if (flag === 'd')
360
- ask.repeated = true;
361
- if (flag === 'u')
362
- ask.unique = true;
363
- if (flag === 'i')
364
- ask.fold = true;
365
- }
366
- // A second operand is uniq's output file, written on the host: the system uniq's.
367
- const foreign = read.foreign ?? (read.operands.length > 1 ? read.operands[1] : null);
368
- return { ask, operands: read.operands, foreign, refused: null };
703
+ return declaredCommand_run(sortCommand, args);
369
704
  }
370
- /** Adjacent equal lines folded to one, as `uniq` does. */
705
+ /** What a `uniq` line may hold: one input at most; a second operand, uniq's output file, is the system uniq's. */
706
+ const UNIQ_GRAMMAR = {
707
+ operands: [{ name: 'file', kind: 'cfsPath', optional: true, glob: true, summary: 'A ChRIS file to read (- is the pipe); the pipe when none is named' }],
708
+ options: {
709
+ count: { short: 'c', long: 'count', summary: 'Prefix each with its count' },
710
+ repeated: { short: 'd', long: 'repeated', summary: 'Only repeated lines' },
711
+ unique: { short: 'u', long: 'unique', summary: 'Only lines never repeated' },
712
+ fold: { short: 'i', long: 'ignore-case', summary: 'Ignore case' },
713
+ },
714
+ };
715
+ /**
716
+ * Adjacent equal lines folded to one, as `uniq` does.
717
+ *
718
+ * @param ask - What it keeps.
719
+ * @param lines - The lines.
720
+ * @returns The kept lines.
721
+ *
722
+ * @example
723
+ * ```ts
724
+ * uniq_lines({ count: false, repeated: false, unique: false, fold: false }, ['a', 'a', 'b']); // ['a', 'b']
725
+ * ```
726
+ */
371
727
  export function uniq_lines(ask, lines) {
372
728
  const out = [];
373
729
  const same = (a, b) => (ask.fold ? a.toLowerCase() === b.toLowerCase() : a === b);
@@ -383,11 +739,62 @@ export function uniq_lines(ask, lines) {
383
739
  }
384
740
  return out;
385
741
  }
386
- /** `uniq [-cdui] [FILE]` */
742
+ /**
743
+ * `uniq [-cdui] [FILE]`: the declaration.
744
+ *
745
+ * @example
746
+ * ```ts
747
+ * commands_register({ declarations: [uniqCommand] });
748
+ * ```
749
+ */
750
+ export const uniqCommand = command_declare({
751
+ name: 'uniq',
752
+ grammar: UNIQ_GRAMMAR,
753
+ help: {
754
+ summary: 'Fold adjacent equal lines to one',
755
+ description: 'Folds each run of equal adjacent lines to one, of what is piped in or of one file. Sort first to fold every equal line. It runs in the session, on every surface; a flag it does not take, or an output file, is refused, and command uniq runs the system uniq.',
756
+ category: 'Files',
757
+ posix: { group: POSIX_GROUP, differs: 'as uniq, without an output file; a FILE is a ChRIS path' },
758
+ examples: ['cut -d , -f 1 a.csv | sort | uniq -c', 'ls | uniq'],
759
+ },
760
+ holds: [],
761
+ handler: async (args) => {
762
+ const ask = { count: args.options.count, repeated: args.options.repeated, unique: args.options.unique, fold: args.options.fold };
763
+ const files = args.operands.file === undefined ? [] : [args.operands.file];
764
+ return filter_run('uniq', files, (sources) => lines_join(uniq_lines(ask, sourceLines_list(sources))));
765
+ },
766
+ });
767
+ /**
768
+ * `uniq` over words already split: the declaration's parse and handler.
769
+ *
770
+ * @param args - The words after `uniq`.
771
+ * @returns The folded lines, or the refusal of a bad line.
772
+ *
773
+ * @example
774
+ * ```ts
775
+ * await builtin_uniq(['-c']);
776
+ * ```
777
+ */
387
778
  export async function builtin_uniq(args) {
388
- return filter_run('uniq', args, uniqArgs_parse(args), (ask, sources) => lines_join(uniq_lines(ask, sources.flatMap((s) => lines_split(s.text)))));
779
+ return declaredCommand_run(uniqCommand, args);
389
780
  }
390
- /** Reads a cut LIST: `1,3`, `2-4`, `3-`, `-2`. */
781
+ /** What a `cut` line may hold. */
782
+ const CUT_GRAMMAR = {
783
+ operands: [FILES_OPERAND],
784
+ options: {
785
+ fields: { short: 'f', long: 'fields', value: { name: 'LIST', kind: 'word' }, summary: 'Keep these fields' },
786
+ chars: { short: 'c', long: 'characters', value: { name: 'LIST', kind: 'word' }, summary: 'Keep these characters' },
787
+ bytes: { short: 'b', long: 'bytes', value: { name: 'LIST', kind: 'word' }, summary: 'Keep these characters (as -c)' },
788
+ delimiter: { short: 'd', long: 'delimiter', value: { name: 'C', kind: 'word' }, summary: 'The field delimiter (a tab when not said)' },
789
+ onlyDelimited: { short: 's', long: 'only-delimited', summary: 'Only lines that hold the delimiter' },
790
+ },
791
+ };
792
+ /**
793
+ * Reads a cut LIST: `1,3`, `2-4`, `3-`, `-2`.
794
+ *
795
+ * @param list - The LIST.
796
+ * @returns Its ranges, or null when it is not one.
797
+ */
391
798
  function cutList_parse(list) {
392
799
  const ranges = [];
393
800
  for (const part of list.split(',')) {
@@ -402,37 +809,43 @@ function cutList_parse(list) {
402
809
  }
403
810
  return ranges;
404
811
  }
405
- /** Reads `cut`'s words. */
406
- export function cutArgs_parse(args) {
407
- const ask = { by: 'fields', ranges: [], delimiter: '\t', onlyDelimited: false };
408
- const read = flags_read(args, 's', 'fcbd', { '--fields': 'f', '--characters': 'c', '--bytes': 'b', '--delimiter': 'd', '--only-delimited': 's' });
409
- if (read.missing !== null)
410
- return { ask, operands: read.operands, foreign: null, refused: `cut: ${read.missing} needs an argument` };
411
- let listed = false;
412
- for (const [flag, value] of read.flags) {
413
- if (flag === 's')
414
- ask.onlyDelimited = true;
415
- if (flag === 'd') {
416
- if ((value ?? '').length !== 1)
417
- return { ask, operands: read.operands, foreign: null, refused: `cut: -d takes one character, not '${value ?? ''}'` };
418
- ask.delimiter = value;
419
- }
420
- if (flag === 'f' || flag === 'c' || flag === 'b') {
421
- if (listed)
422
- return { ask, operands: read.operands, foreign: null, refused: 'cut: only one of -f, -c or -b' };
423
- const ranges = cutList_parse(value ?? '');
424
- if (ranges === null)
425
- return { ask, operands: read.operands, foreign: null, refused: `cut: a list is N, N-M, N- or -M joined by commas, not '${value ?? ''}'` };
426
- ask.by = flag === 'f' ? 'fields' : 'chars';
427
- ask.ranges = ranges;
428
- listed = true;
429
- }
812
+ /**
813
+ * Reads `cut`'s options: one LIST, of fields or characters, and a one-character delimiter.
814
+ *
815
+ * @param options - The typed options.
816
+ * @returns What they ask, or the refusal of a list or delimiter it does not take.
817
+ */
818
+ function cutAsk_read(options) {
819
+ const ask = { by: 'fields', ranges: [], delimiter: '\t', onlyDelimited: options.onlyDelimited };
820
+ if (options.delimiter !== undefined) {
821
+ if (options.delimiter.length !== 1)
822
+ return { refused: `cut: -d takes one character, not '${options.delimiter}'` };
823
+ ask.delimiter = options.delimiter;
430
824
  }
431
- if (!listed && read.foreign === null)
432
- return { ask, operands: read.operands, foreign: null, refused: 'cut: say what to keep: -f LIST (fields) or -c LIST (characters)' };
433
- return { ask, operands: read.operands, foreign: read.foreign, refused: null };
825
+ const lists = [
826
+ ...(options.fields !== undefined ? [{ by: 'fields', list: options.fields }] : []),
827
+ ...(options.chars !== undefined ? [{ by: 'chars', list: options.chars }] : []),
828
+ ...(options.bytes !== undefined ? [{ by: 'chars', list: options.bytes }] : []),
829
+ ];
830
+ if (lists.length > 1)
831
+ return { refused: 'cut: only one of -f, -c or -b' };
832
+ const chosen = lists[0];
833
+ if (chosen === undefined)
834
+ return { refused: 'cut: say what to keep: -f LIST (fields) or -c LIST (characters)' };
835
+ const ranges = cutList_parse(chosen.list);
836
+ if (ranges === null)
837
+ return { refused: `cut: a list is N, N-M, N- or -M joined by commas, not '${chosen.list}'` };
838
+ ask.by = chosen.by;
839
+ ask.ranges = ranges;
840
+ return { ask };
434
841
  }
435
- /** The positions a cut keeps of `n`, in order, each once. */
842
+ /**
843
+ * The positions a cut keeps of `n`, in order, each once.
844
+ *
845
+ * @param ask - What it keeps.
846
+ * @param n - How many fields or characters the line has.
847
+ * @returns The positions, 1-based.
848
+ */
436
849
  function cutPositions_derive(ask, n) {
437
850
  const kept = new Set();
438
851
  for (const range of ask.ranges)
@@ -440,7 +853,18 @@ function cutPositions_derive(ask, n) {
440
853
  kept.add(i);
441
854
  return [...kept].sort((a, b) => a - b);
442
855
  }
443
- /** What `cut` keeps of each line. */
856
+ /**
857
+ * What `cut` keeps of each line.
858
+ *
859
+ * @param ask - What it keeps.
860
+ * @param lines - The lines.
861
+ * @returns The kept parts, a line each (a line with no delimiter dropped under -s).
862
+ *
863
+ * @example
864
+ * ```ts
865
+ * cut_lines({ by: 'fields', ranges: [{ from: 2, to: 2 }], delimiter: ',', onlyDelimited: false }, ['a,b']); // ['b']
866
+ * ```
867
+ */
444
868
  export function cut_lines(ask, lines) {
445
869
  const out = [];
446
870
  for (const line of lines) {
@@ -459,8 +883,39 @@ export function cut_lines(ask, lines) {
459
883
  }
460
884
  return out;
461
885
  }
462
- /** `cut -f LIST [-d C] [-s] | -c LIST [FILE]...` */
886
+ /**
887
+ * `cut -f LIST [-d C] [-s] | -c LIST [FILE]...`: the declaration.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * commands_register({ declarations: [cutCommand] });
892
+ * ```
893
+ */
894
+ export const cutCommand = command_declare({
895
+ name: 'cut',
896
+ grammar: CUT_GRAMMAR,
897
+ help: {
898
+ summary: 'Keep fields or characters of each line',
899
+ description: 'Keeps the fields (split at -d C, a tab when not said) or the characters LIST names, of each line piped in or of the files named. A LIST is N, N-M, N- or -M, joined by commas; -b is -c. -s drops a line with no delimiter. It runs in the session, on every surface; a flag it does not take is refused, and command cut runs the system cut.',
900
+ category: 'Files',
901
+ posix: { group: POSIX_GROUP, differs: 'as cut, -b as -c; a FILE is a ChRIS path' },
902
+ examples: ['cut -d , -f 1,3 a.csv', 'ls | cut -c 1-8'],
903
+ },
904
+ holds: [],
905
+ handler: async (args) => askedFilter_run('cut', cutAsk_read(args.options), args.operands.files, (ask, sources) => lines_join(cut_lines(ask, sourceLines_list(sources)))),
906
+ });
907
+ /**
908
+ * `cut` over words already split: the declaration's parse and handler.
909
+ *
910
+ * @param args - The words after `cut`.
911
+ * @returns The kept parts, or the refusal of a bad line.
912
+ *
913
+ * @example
914
+ * ```ts
915
+ * await builtin_cut(['-d', ',', '-f', '1', 'a.csv']);
916
+ * ```
917
+ */
463
918
  export async function builtin_cut(args) {
464
- return filter_run('cut', args, cutArgs_parse(args), (ask, sources) => lines_join(cut_lines(ask, sources.flatMap((s) => lines_split(s.text)))));
919
+ return declaredCommand_run(cutCommand, args);
465
920
  }
466
921
  //# sourceMappingURL=textFilters.js.map