@seanhogg/builderforce-webcontainers-core 2026.10.1 → 2026.10.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.
- package/dist/build.d.ts +42 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +65 -0
- package/dist/build.js.map +1 -0
- package/dist/buildHost.d.ts +31 -0
- package/dist/buildHost.d.ts.map +1 -0
- package/dist/buildHost.js +125 -0
- package/dist/buildHost.js.map +1 -0
- package/dist/buildHtml.d.ts +28 -0
- package/dist/buildHtml.d.ts.map +1 -0
- package/dist/buildHtml.js +74 -0
- package/dist/buildHtml.js.map +1 -0
- package/dist/bundler.d.ts +129 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +79 -0
- package/dist/bundler.js.map +1 -0
- package/dist/check/acquire.d.ts +46 -0
- package/dist/check/acquire.d.ts.map +1 -0
- package/dist/check/acquire.js +179 -0
- package/dist/check/acquire.js.map +1 -0
- package/dist/check/compilerHost.d.ts +22 -0
- package/dist/check/compilerHost.d.ts.map +1 -0
- package/dist/check/compilerHost.js +128 -0
- package/dist/check/compilerHost.js.map +1 -0
- package/dist/check/diagnostics.d.ts +23 -0
- package/dist/check/diagnostics.d.ts.map +1 -0
- package/dist/check/diagnostics.js +28 -0
- package/dist/check/diagnostics.js.map +1 -0
- package/dist/check/index.d.ts +19 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +13 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/projects.d.ts +26 -0
- package/dist/check/projects.d.ts.map +1 -0
- package/dist/check/projects.js +124 -0
- package/dist/check/projects.js.map +1 -0
- package/dist/check/shims.d.ts +29 -0
- package/dist/check/shims.d.ts.map +1 -0
- package/dist/check/shims.js +76 -0
- package/dist/check/shims.js.map +1 -0
- package/dist/check/typeStore.d.ts +57 -0
- package/dist/check/typeStore.d.ts.map +1 -0
- package/dist/check/typeStore.js +89 -0
- package/dist/check/typeStore.js.map +1 -0
- package/dist/check/typecheck.d.ts +29 -0
- package/dist/check/typecheck.d.ts.map +1 -0
- package/dist/check/typecheck.js +91 -0
- package/dist/check/typecheck.js.map +1 -0
- package/dist/compileComponent.d.ts +8 -0
- package/dist/compileComponent.d.ts.map +1 -0
- package/dist/compileComponent.js +22 -0
- package/dist/compileComponent.js.map +1 -0
- package/dist/compileScript.d.ts +19 -4
- package/dist/compileScript.d.ts.map +1 -1
- package/dist/compileScript.js +37 -12
- package/dist/compileScript.js.map +1 -1
- package/dist/components.d.ts +41 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +14 -0
- package/dist/components.js.map +1 -0
- package/dist/detectProject.d.ts +3 -0
- package/dist/detectProject.d.ts.map +1 -1
- package/dist/detectProject.js +16 -6
- package/dist/detectProject.js.map +1 -1
- package/dist/devServer.d.ts +5 -2
- package/dist/devServer.d.ts.map +1 -1
- package/dist/devServer.js +16 -8
- package/dist/devServer.js.map +1 -1
- package/dist/hash.d.ts +2 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +22 -0
- package/dist/hash.js.map +1 -0
- package/dist/html.d.ts +8 -0
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +17 -1
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +25 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/installer/bins.d.ts +24 -0
- package/dist/installer/bins.d.ts.map +1 -0
- package/dist/installer/bins.js +56 -0
- package/dist/installer/bins.js.map +1 -0
- package/dist/installer/index.d.ts +14 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +9 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/install.d.ts +46 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +245 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/installer/lockfile.d.ts +49 -0
- package/dist/installer/lockfile.d.ts.map +1 -0
- package/dist/installer/lockfile.js +110 -0
- package/dist/installer/lockfile.js.map +1 -0
- package/dist/installer/registry.d.ts +84 -0
- package/dist/installer/registry.d.ts.map +1 -0
- package/dist/installer/registry.js +130 -0
- package/dist/installer/registry.js.map +1 -0
- package/dist/installer/semver.d.ts +38 -0
- package/dist/installer/semver.d.ts.map +1 -0
- package/dist/installer/semver.js +187 -0
- package/dist/installer/semver.js.map +1 -0
- package/dist/installer/spec.d.ts +30 -0
- package/dist/installer/spec.d.ts.map +1 -0
- package/dist/installer/spec.js +30 -0
- package/dist/installer/spec.js.map +1 -0
- package/dist/installer/tar.d.ts +17 -0
- package/dist/installer/tar.d.ts.map +1 -0
- package/dist/installer/tar.js +150 -0
- package/dist/installer/tar.js.map +1 -0
- package/dist/installer/tree.d.ts +48 -0
- package/dist/installer/tree.d.ts.map +1 -0
- package/dist/installer/tree.js +179 -0
- package/dist/installer/tree.js.map +1 -0
- package/dist/node/assert.d.ts +15 -0
- package/dist/node/assert.d.ts.map +1 -0
- package/dist/node/assert.js +137 -0
- package/dist/node/assert.js.map +1 -0
- package/dist/node/buffer.d.ts +84 -0
- package/dist/node/buffer.d.ts.map +1 -0
- package/dist/node/buffer.js +304 -0
- package/dist/node/buffer.js.map +1 -0
- package/dist/node/builtins.d.ts +25 -0
- package/dist/node/builtins.d.ts.map +1 -0
- package/dist/node/builtins.js +101 -0
- package/dist/node/builtins.js.map +1 -0
- package/dist/node/callable.d.ts +13 -0
- package/dist/node/callable.d.ts.map +1 -0
- package/dist/node/callable.js +26 -0
- package/dist/node/callable.js.map +1 -0
- package/dist/node/childProcess.d.ts +13 -0
- package/dist/node/childProcess.d.ts.map +1 -0
- package/dist/node/childProcess.js +143 -0
- package/dist/node/childProcess.js.map +1 -0
- package/dist/node/crypto.d.ts +3 -0
- package/dist/node/crypto.d.ts.map +1 -0
- package/dist/node/crypto.js +180 -0
- package/dist/node/crypto.js.map +1 -0
- package/dist/node/esm.d.ts +9 -0
- package/dist/node/esm.d.ts.map +1 -0
- package/dist/node/esm.js +202 -0
- package/dist/node/esm.js.map +1 -0
- package/dist/node/events.d.ts +40 -0
- package/dist/node/events.d.ts.map +1 -0
- package/dist/node/events.js +160 -0
- package/dist/node/events.js.map +1 -0
- package/dist/node/fs/index.d.ts +7 -0
- package/dist/node/fs/index.d.ts.map +1 -0
- package/dist/node/fs/index.js +270 -0
- package/dist/node/fs/index.js.map +1 -0
- package/dist/node/fs/stats.d.ts +93 -0
- package/dist/node/fs/stats.d.ts.map +1 -0
- package/dist/node/fs/stats.js +104 -0
- package/dist/node/fs/stats.js.map +1 -0
- package/dist/node/fs/sync.d.ts +98 -0
- package/dist/node/fs/sync.d.ts.map +1 -0
- package/dist/node/fs/sync.js +348 -0
- package/dist/node/fs/sync.js.map +1 -0
- package/dist/node/hashes.d.ts +15 -0
- package/dist/node/hashes.d.ts.map +1 -0
- package/dist/node/hashes.js +195 -0
- package/dist/node/hashes.js.map +1 -0
- package/dist/node/http/client.d.ts +35 -0
- package/dist/node/http/client.d.ts.map +1 -0
- package/dist/node/http/client.js +161 -0
- package/dist/node/http/client.js.map +1 -0
- package/dist/node/http/index.d.ts +5 -0
- package/dist/node/http/index.d.ts.map +1 -0
- package/dist/node/http/index.js +125 -0
- package/dist/node/http/index.js.map +1 -0
- package/dist/node/http/server.d.ts +135 -0
- package/dist/node/http/server.d.ts.map +1 -0
- package/dist/node/http/server.js +308 -0
- package/dist/node/http/server.js.map +1 -0
- package/dist/node/http/status.d.ts +3 -0
- package/dist/node/http/status.d.ts.map +1 -0
- package/dist/node/http/status.js +13 -0
- package/dist/node/http/status.js.map +1 -0
- package/dist/node/inspect.d.ts +22 -0
- package/dist/node/inspect.d.ts.map +1 -0
- package/dist/node/inspect.js +172 -0
- package/dist/node/inspect.js.map +1 -0
- package/dist/node/loader.d.ts +91 -0
- package/dist/node/loader.d.ts.map +1 -0
- package/dist/node/loader.js +265 -0
- package/dist/node/loader.js.map +1 -0
- package/dist/node/loop.d.ts +71 -0
- package/dist/node/loop.d.ts.map +1 -0
- package/dist/node/loop.js +189 -0
- package/dist/node/loop.js.map +1 -0
- package/dist/node/misc.d.ts +10 -0
- package/dist/node/misc.d.ts.map +1 -0
- package/dist/node/misc.js +256 -0
- package/dist/node/misc.js.map +1 -0
- package/dist/node/path.d.ts +14 -0
- package/dist/node/path.d.ts.map +1 -0
- package/dist/node/path.js +128 -0
- package/dist/node/path.js.map +1 -0
- package/dist/node/process.d.ts +31 -0
- package/dist/node/process.d.ts.map +1 -0
- package/dist/node/process.js +309 -0
- package/dist/node/process.js.map +1 -0
- package/dist/node/program.d.ts +19 -0
- package/dist/node/program.d.ts.map +1 -0
- package/dist/node/program.js +165 -0
- package/dist/node/program.js.map +1 -0
- package/dist/node/readline.d.ts +2 -0
- package/dist/node/readline.d.ts.map +1 -0
- package/dist/node/readline.js +155 -0
- package/dist/node/readline.js.map +1 -0
- package/dist/node/resolve.d.ts +45 -0
- package/dist/node/resolve.d.ts.map +1 -0
- package/dist/node/resolve.js +206 -0
- package/dist/node/resolve.js.map +1 -0
- package/dist/node/runtime.d.ts +40 -0
- package/dist/node/runtime.d.ts.map +1 -0
- package/dist/node/runtime.js +149 -0
- package/dist/node/runtime.js.map +1 -0
- package/dist/node/stream/index.d.ts +52 -0
- package/dist/node/stream/index.d.ts.map +1 -0
- package/dist/node/stream/index.js +184 -0
- package/dist/node/stream/index.js.map +1 -0
- package/dist/node/stream/readable.d.ts +83 -0
- package/dist/node/stream/readable.d.ts.map +1 -0
- package/dist/node/stream/readable.js +325 -0
- package/dist/node/stream/readable.js.map +1 -0
- package/dist/node/stream/writable.d.ts +80 -0
- package/dist/node/stream/writable.d.ts.map +1 -0
- package/dist/node/stream/writable.js +224 -0
- package/dist/node/stream/writable.js.map +1 -0
- package/dist/node/urlqs.d.ts +22 -0
- package/dist/node/urlqs.d.ts.map +1 -0
- package/dist/node/urlqs.js +165 -0
- package/dist/node/urlqs.js.map +1 -0
- package/dist/node/util.d.ts +37 -0
- package/dist/node/util.d.ts.map +1 -0
- package/dist/node/util.js +160 -0
- package/dist/node/util.js.map +1 -0
- package/dist/node/zlib.d.ts +2 -0
- package/dist/node/zlib.d.ts.map +1 -0
- package/dist/node/zlib.js +133 -0
- package/dist/node/zlib.js.map +1 -0
- package/dist/packageCdn.d.ts +12 -0
- package/dist/packageCdn.d.ts.map +1 -1
- package/dist/packageCdn.js +3 -1
- package/dist/packageCdn.js.map +1 -1
- package/dist/projectConfig.d.ts +6 -2
- package/dist/projectConfig.d.ts.map +1 -1
- package/dist/projectConfig.js +32 -8
- package/dist/projectConfig.js.map +1 -1
- package/dist/shell/builtins.d.ts +23 -0
- package/dist/shell/builtins.d.ts.map +1 -0
- package/dist/shell/builtins.js +131 -0
- package/dist/shell/builtins.js.map +1 -0
- package/dist/shell/context.d.ts +41 -0
- package/dist/shell/context.d.ts.map +1 -0
- package/dist/shell/context.js +65 -0
- package/dist/shell/context.js.map +1 -0
- package/dist/shell/devHandoff.d.ts +18 -0
- package/dist/shell/devHandoff.d.ts.map +1 -0
- package/dist/shell/devHandoff.js +36 -0
- package/dist/shell/devHandoff.js.map +1 -0
- package/dist/shell/exec.d.ts +67 -0
- package/dist/shell/exec.d.ts.map +1 -0
- package/dist/shell/exec.js +247 -0
- package/dist/shell/exec.js.map +1 -0
- package/dist/shell/expand.d.ts +28 -0
- package/dist/shell/expand.d.ts.map +1 -0
- package/dist/shell/expand.js +149 -0
- package/dist/shell/expand.js.map +1 -0
- package/dist/shell/fileCommands.d.ts +10 -0
- package/dist/shell/fileCommands.d.ts.map +1 -0
- package/dist/shell/fileCommands.js +208 -0
- package/dist/shell/fileCommands.js.map +1 -0
- package/dist/shell/lineEditor.d.ts +43 -0
- package/dist/shell/lineEditor.d.ts.map +1 -0
- package/dist/shell/lineEditor.js +234 -0
- package/dist/shell/lineEditor.js.map +1 -0
- package/dist/shell/npm.d.ts +6 -0
- package/dist/shell/npm.d.ts.map +1 -0
- package/dist/shell/npm.js +234 -0
- package/dist/shell/npm.js.map +1 -0
- package/dist/shell/parse.d.ts +73 -0
- package/dist/shell/parse.d.ts.map +1 -0
- package/dist/shell/parse.js +300 -0
- package/dist/shell/parse.js.map +1 -0
- package/dist/shell/program.d.ts +12 -0
- package/dist/shell/program.d.ts.map +1 -0
- package/dist/shell/program.js +219 -0
- package/dist/shell/program.js.map +1 -0
- package/dist/shell/textCommands.d.ts +15 -0
- package/dist/shell/textCommands.d.ts.map +1 -0
- package/dist/shell/textCommands.js +185 -0
- package/dist/shell/textCommands.js.map +1 -0
- package/dist/styleModules.d.ts +6 -0
- package/dist/styleModules.d.ts.map +1 -1
- package/dist/styleModules.js +14 -4
- package/dist/styleModules.js.map +1 -1
- package/dist/svelteCompiler.d.ts +28 -0
- package/dist/svelteCompiler.d.ts.map +1 -0
- package/dist/svelteCompiler.js +13 -0
- package/dist/svelteCompiler.js.map +1 -0
- package/dist/system/commands.d.ts +26 -0
- package/dist/system/commands.d.ts.map +1 -0
- package/dist/system/commands.js +53 -0
- package/dist/system/commands.js.map +1 -0
- package/dist/system/index.d.ts +23 -0
- package/dist/system/index.d.ts.map +1 -0
- package/dist/system/index.js +16 -0
- package/dist/system/index.js.map +1 -0
- package/dist/system/kernel.d.ts +54 -0
- package/dist/system/kernel.d.ts.map +1 -0
- package/dist/system/kernel.js +81 -0
- package/dist/system/kernel.js.map +1 -0
- package/dist/system/ports.d.ts +32 -0
- package/dist/system/ports.d.ts.map +1 -0
- package/dist/system/ports.js +57 -0
- package/dist/system/ports.js.map +1 -0
- package/dist/system/process.d.ts +32 -0
- package/dist/system/process.d.ts.map +1 -0
- package/dist/system/process.js +130 -0
- package/dist/system/process.js.map +1 -0
- package/dist/system/programs.d.ts +10 -0
- package/dist/system/programs.d.ts.map +1 -0
- package/dist/system/programs.js +22 -0
- package/dist/system/programs.js.map +1 -0
- package/dist/system/terminal.d.ts +45 -0
- package/dist/system/terminal.d.ts.map +1 -0
- package/dist/system/terminal.js +127 -0
- package/dist/system/terminal.js.map +1 -0
- package/dist/system/types.d.ts +100 -0
- package/dist/system/types.d.ts.map +1 -0
- package/dist/system/types.js +2 -0
- package/dist/system/types.js.map +1 -0
- package/dist/vfs.d.ts +28 -4
- package/dist/vfs.d.ts.map +1 -1
- package/dist/vfs.js +95 -32
- package/dist/vfs.js.map +1 -1
- package/dist/vueCompiler.d.ts +75 -0
- package/dist/vueCompiler.d.ts.map +1 -0
- package/dist/vueCompiler.js +67 -0
- package/dist/vueCompiler.js.map +1 -0
- package/package.json +18 -2
- package/src/build.ts +105 -0
- package/src/buildHost.ts +154 -0
- package/src/buildHtml.ts +101 -0
- package/src/bundler.ts +190 -0
- package/src/check/acquire.ts +212 -0
- package/src/check/compilerHost.ts +138 -0
- package/src/check/diagnostics.ts +48 -0
- package/src/check/index.ts +28 -0
- package/src/check/projects.ts +149 -0
- package/src/check/shims.ts +88 -0
- package/src/check/typeStore.ts +103 -0
- package/src/check/typecheck.ts +134 -0
- package/src/compileComponent.ts +21 -0
- package/src/compileScript.ts +42 -13
- package/src/components.ts +59 -0
- package/src/detectProject.ts +18 -6
- package/src/devServer.ts +19 -10
- package/src/hash.ts +22 -0
- package/src/html.ts +19 -1
- package/src/index.ts +36 -7
- package/src/installer/bins.ts +64 -0
- package/src/installer/index.ts +13 -0
- package/src/installer/install.ts +275 -0
- package/src/installer/lockfile.ts +146 -0
- package/src/installer/registry.ts +175 -0
- package/src/installer/semver.ts +190 -0
- package/src/installer/spec.ts +53 -0
- package/src/installer/tar.ts +155 -0
- package/src/installer/tree.ts +236 -0
- package/src/node/assert.ts +127 -0
- package/src/node/buffer.ts +304 -0
- package/src/node/builtins.ts +123 -0
- package/src/node/callable.ts +25 -0
- package/src/node/childProcess.ts +164 -0
- package/src/node/crypto.ts +187 -0
- package/src/node/esm.ts +196 -0
- package/src/node/events.ts +176 -0
- package/src/node/fs/index.ts +277 -0
- package/src/node/fs/stats.ts +110 -0
- package/src/node/fs/sync.ts +335 -0
- package/src/node/hashes.ts +171 -0
- package/src/node/http/client.ts +173 -0
- package/src/node/http/index.ts +122 -0
- package/src/node/http/server.ts +308 -0
- package/src/node/http/status.ts +13 -0
- package/src/node/inspect.ts +166 -0
- package/src/node/loader.ts +293 -0
- package/src/node/loop.ts +193 -0
- package/src/node/misc.ts +250 -0
- package/src/node/path.ts +135 -0
- package/src/node/process.ts +324 -0
- package/src/node/program.ts +170 -0
- package/src/node/readline.ts +157 -0
- package/src/node/resolve.ts +216 -0
- package/src/node/runtime.ts +184 -0
- package/src/node/stream/index.ts +196 -0
- package/src/node/stream/readable.ts +345 -0
- package/src/node/stream/writable.ts +244 -0
- package/src/node/urlqs.ts +162 -0
- package/src/node/util.ts +145 -0
- package/src/node/zlib.ts +145 -0
- package/src/packageCdn.ts +11 -1
- package/src/projectConfig.ts +34 -7
- package/src/shell/builtins.ts +141 -0
- package/src/shell/context.ts +89 -0
- package/src/shell/devHandoff.ts +49 -0
- package/src/shell/exec.ts +268 -0
- package/src/shell/expand.ts +145 -0
- package/src/shell/fileCommands.ts +206 -0
- package/src/shell/lineEditor.ts +233 -0
- package/src/shell/npm.ts +229 -0
- package/src/shell/parse.ts +295 -0
- package/src/shell/program.ts +223 -0
- package/src/shell/textCommands.ts +177 -0
- package/src/styleModules.ts +15 -4
- package/src/svelteCompiler.ts +31 -0
- package/src/system/commands.ts +72 -0
- package/src/system/index.ts +22 -0
- package/src/system/kernel.ts +106 -0
- package/src/system/ports.ts +73 -0
- package/src/system/process.ts +158 -0
- package/src/system/programs.ts +31 -0
- package/src/system/terminal.ts +135 -0
- package/src/system/types.ts +103 -0
- package/src/vfs.ts +92 -28
- package/src/vueCompiler.ts +121 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell grammar, a practical subset of POSIX sh:
|
|
3
|
+
*
|
|
4
|
+
* script := list (('\n' | ';' | '&') list)*
|
|
5
|
+
* list := pipeline (('&&' | '||') pipeline)*
|
|
6
|
+
* pipeline := ['!'] command ('|' command)*
|
|
7
|
+
* command := (NAME=word)* word* redirect*
|
|
8
|
+
* redirect := ('>' | '>>' | '<' | '2>' | '2>>' | '&>') word | '2>&1' | '>&2'
|
|
9
|
+
*
|
|
10
|
+
* Words keep their quoting as segments, because expansion (variables, `$(…)`,
|
|
11
|
+
* `~`, globs, field splitting) happens at run time and depends on it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Segment =
|
|
15
|
+
| { kind: 'text'; value: string; quoted: boolean }
|
|
16
|
+
| { kind: 'var'; name: string; quoted: boolean; op?: ':-' | '-' | ':=' | ':+'; arg?: Word }
|
|
17
|
+
| { kind: 'cmd'; source: string; quoted: boolean }
|
|
18
|
+
| { kind: 'tilde' };
|
|
19
|
+
export type Word = Segment[];
|
|
20
|
+
|
|
21
|
+
export type RedirectOp = '>' | '>>' | '<' | '2>' | '2>>' | '&>' | '2>&1' | '>&2';
|
|
22
|
+
export interface Redirect {
|
|
23
|
+
op: RedirectOp;
|
|
24
|
+
target?: Word;
|
|
25
|
+
}
|
|
26
|
+
export interface Command {
|
|
27
|
+
assignments: Array<{ name: string; value: Word }>;
|
|
28
|
+
words: Word[];
|
|
29
|
+
redirects: Redirect[];
|
|
30
|
+
}
|
|
31
|
+
export interface Pipeline {
|
|
32
|
+
negate: boolean;
|
|
33
|
+
commands: Command[];
|
|
34
|
+
}
|
|
35
|
+
export interface AndOrList {
|
|
36
|
+
first: Pipeline;
|
|
37
|
+
rest: Array<{ op: '&&' | '||'; pipeline: Pipeline }>;
|
|
38
|
+
background: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type Script = AndOrList[];
|
|
41
|
+
|
|
42
|
+
type Token = { type: 'word'; word: Word; raw: string } | { type: 'op'; op: string };
|
|
43
|
+
|
|
44
|
+
export class ShellSyntaxError extends Error {
|
|
45
|
+
/** The input ended inside a quote or after `|`/`&&`: an interactive shell asks for more. */
|
|
46
|
+
constructor(message: string, readonly incomplete = false) {
|
|
47
|
+
super(message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const OPERATORS = ['2>&1', '>&2', '2>>', '&&', '||', '>>', '2>', '&>', '|', '&', ';', '>', '<', '\n'];
|
|
52
|
+
const SPECIAL_VARS = /^[?$#@*!0-9]/;
|
|
53
|
+
|
|
54
|
+
/** Find the `)` closing a `$(` at `start` (just after the paren), honouring quotes and nesting. */
|
|
55
|
+
function matchParen(input: string, start: number): number {
|
|
56
|
+
let depth = 1;
|
|
57
|
+
for (let i = start; i < input.length; i++) {
|
|
58
|
+
const ch = input[i];
|
|
59
|
+
if (ch === '\\') i++;
|
|
60
|
+
else if (ch === "'") i = input.indexOf("'", i + 1) < 0 ? input.length : input.indexOf("'", i + 1);
|
|
61
|
+
else if (ch === '"') {
|
|
62
|
+
for (i++; i < input.length && input[i] !== '"'; i++) if (input[i] === '\\') i++;
|
|
63
|
+
} else if (ch === '(') depth++;
|
|
64
|
+
else if (ch === ')' && --depth === 0) return i;
|
|
65
|
+
}
|
|
66
|
+
throw new ShellSyntaxError('unexpected end of input: missing )', true);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parse `$…` at `i` (pointing at `$`); returns the segment and the index after it. */
|
|
70
|
+
function readDollar(input: string, i: number, quoted: boolean): [Segment, number] {
|
|
71
|
+
const next = input[i + 1];
|
|
72
|
+
if (next === '(') {
|
|
73
|
+
const end = matchParen(input, i + 2);
|
|
74
|
+
return [{ kind: 'cmd', source: input.slice(i + 2, end), quoted }, end + 1];
|
|
75
|
+
}
|
|
76
|
+
if (next === '{') {
|
|
77
|
+
const end = input.indexOf('}', i + 2);
|
|
78
|
+
if (end < 0) throw new ShellSyntaxError('unexpected end of input: missing }', true);
|
|
79
|
+
const body = input.slice(i + 2, end);
|
|
80
|
+
const m = /^([A-Za-z_][\w]*|[?$#@*!0-9])(?:(:-|-|:=|:\+)(.*))?$/s.exec(body);
|
|
81
|
+
if (!m) throw new ShellSyntaxError(`bad substitution: \${${body}}`);
|
|
82
|
+
const seg: Segment = { kind: 'var', name: m[1]!, quoted };
|
|
83
|
+
if (m[2]) {
|
|
84
|
+
seg.op = m[2] as ':-';
|
|
85
|
+
seg.arg = tokenizeWord(m[3] ?? '', quoted);
|
|
86
|
+
}
|
|
87
|
+
return [seg, end + 1];
|
|
88
|
+
}
|
|
89
|
+
if (next && SPECIAL_VARS.test(next)) return [{ kind: 'var', name: next, quoted }, i + 2];
|
|
90
|
+
const m = /^[A-Za-z_]\w*/.exec(input.slice(i + 1));
|
|
91
|
+
if (!m) return [{ kind: 'text', value: '$', quoted }, i + 1];
|
|
92
|
+
return [{ kind: 'var', name: m[0], quoted }, i + 1 + m[0].length];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Segments of a word body (used for `${x:-default}` arguments). */
|
|
96
|
+
function tokenizeWord(text: string, quoted: boolean): Word {
|
|
97
|
+
const tokens = tokenize(text, true);
|
|
98
|
+
const word: Word = [];
|
|
99
|
+
for (const t of tokens) if (t.type === 'word') word.push(...t.word.map((s) => (s.kind === 'tilde' ? s : { ...s, quoted: quoted || ('quoted' in s && s.quoted) } as Segment)));
|
|
100
|
+
return word;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function tokenize(input: string, wordOnly = false): Token[] {
|
|
104
|
+
const tokens: Token[] = [];
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < input.length) {
|
|
107
|
+
const ch = input[i]!;
|
|
108
|
+
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === '\\' && input[i + 1] === '\n') {
|
|
113
|
+
i += 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === '#') {
|
|
117
|
+
while (i < input.length && input[i] !== '\n') i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!wordOnly) {
|
|
121
|
+
const op = OPERATORS.find((o) => input.startsWith(o, i));
|
|
122
|
+
if (op) {
|
|
123
|
+
tokens.push({ type: 'op', op });
|
|
124
|
+
i += op.length;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// A word: runs until unquoted whitespace or an operator character.
|
|
129
|
+
const word: Word = [];
|
|
130
|
+
const start = i;
|
|
131
|
+
let text = '';
|
|
132
|
+
const flush = () => {
|
|
133
|
+
if (text) word.push({ kind: 'text', value: text, quoted: false });
|
|
134
|
+
text = '';
|
|
135
|
+
};
|
|
136
|
+
if (input[i] === '~' && (i + 1 >= input.length || /[\s/;|&<>]/.test(input[i + 1]!))) {
|
|
137
|
+
word.push({ kind: 'tilde' });
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
while (i < input.length) {
|
|
141
|
+
const c = input[i]!;
|
|
142
|
+
if (/[\s]/.test(c) && !wordOnly) break;
|
|
143
|
+
if (!wordOnly && /[;|&<>]/.test(c)) break;
|
|
144
|
+
if (c === '\\') {
|
|
145
|
+
text += input[i + 1] ?? '';
|
|
146
|
+
i += 2;
|
|
147
|
+
} else if (c === "'") {
|
|
148
|
+
const end = input.indexOf("'", i + 1);
|
|
149
|
+
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated quote', true);
|
|
150
|
+
flush();
|
|
151
|
+
word.push({ kind: 'text', value: input.slice(i + 1, end), quoted: true });
|
|
152
|
+
i = end + 1;
|
|
153
|
+
} else if (c === '"') {
|
|
154
|
+
flush();
|
|
155
|
+
i++;
|
|
156
|
+
let inner = '';
|
|
157
|
+
const pushInner = () => {
|
|
158
|
+
if (inner) word.push({ kind: 'text', value: inner, quoted: true });
|
|
159
|
+
inner = '';
|
|
160
|
+
};
|
|
161
|
+
let closed = false;
|
|
162
|
+
while (i < input.length) {
|
|
163
|
+
const d = input[i]!;
|
|
164
|
+
if (d === '"') {
|
|
165
|
+
closed = true;
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (d === '\\' && /["\\$`\n]/.test(input[i + 1] ?? '')) {
|
|
170
|
+
inner += input[i + 1];
|
|
171
|
+
i += 2;
|
|
172
|
+
} else if (d === '$') {
|
|
173
|
+
pushInner();
|
|
174
|
+
const [seg, next] = readDollar(input, i, true);
|
|
175
|
+
word.push(seg);
|
|
176
|
+
i = next;
|
|
177
|
+
} else if (d === '`') {
|
|
178
|
+
pushInner();
|
|
179
|
+
const end = input.indexOf('`', i + 1);
|
|
180
|
+
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated `', true);
|
|
181
|
+
word.push({ kind: 'cmd', source: input.slice(i + 1, end), quoted: true });
|
|
182
|
+
i = end + 1;
|
|
183
|
+
} else {
|
|
184
|
+
inner += d;
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!closed) throw new ShellSyntaxError('unexpected end of input: unterminated quote', true);
|
|
189
|
+
pushInner();
|
|
190
|
+
if (!word.length || word[word.length - 1]!.kind !== 'text') word.push({ kind: 'text', value: '', quoted: true });
|
|
191
|
+
} else if (c === '$') {
|
|
192
|
+
flush();
|
|
193
|
+
const [seg, next] = readDollar(input, i, false);
|
|
194
|
+
word.push(seg);
|
|
195
|
+
i = next;
|
|
196
|
+
} else if (c === '`') {
|
|
197
|
+
flush();
|
|
198
|
+
const end = input.indexOf('`', i + 1);
|
|
199
|
+
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated `', true);
|
|
200
|
+
word.push({ kind: 'cmd', source: input.slice(i + 1, end), quoted: false });
|
|
201
|
+
i = end + 1;
|
|
202
|
+
} else {
|
|
203
|
+
text += c;
|
|
204
|
+
i++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
flush();
|
|
208
|
+
tokens.push({ type: 'word', word, raw: input.slice(start, i) });
|
|
209
|
+
}
|
|
210
|
+
return tokens;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ASSIGNMENT = /^([A-Za-z_]\w*)=/;
|
|
214
|
+
const REDIRECTS = new Set(['>', '>>', '<', '2>', '2>>', '&>', '2>&1', '>&2']);
|
|
215
|
+
|
|
216
|
+
export function parse(input: string): Script {
|
|
217
|
+
const tokens = tokenize(input);
|
|
218
|
+
let pos = 0;
|
|
219
|
+
const peek = () => tokens[pos];
|
|
220
|
+
const isOp = (op: string) => {
|
|
221
|
+
const t = tokens[pos];
|
|
222
|
+
return t?.type === 'op' && t.op === op;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const command = (): Command => {
|
|
226
|
+
const cmd: Command = { assignments: [], words: [], redirects: [] };
|
|
227
|
+
for (;;) {
|
|
228
|
+
const t = peek();
|
|
229
|
+
if (!t) break;
|
|
230
|
+
if (t.type === 'op') {
|
|
231
|
+
if (!REDIRECTS.has(t.op)) break;
|
|
232
|
+
pos++;
|
|
233
|
+
if (t.op === '2>&1' || t.op === '>&2') {
|
|
234
|
+
cmd.redirects.push({ op: t.op });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const target = peek();
|
|
238
|
+
if (target?.type !== 'word') throw new ShellSyntaxError(`syntax error near unexpected token \`${target?.type === 'op' ? target.op.replace('\n', 'newline') : 'newline'}'`);
|
|
239
|
+
pos++;
|
|
240
|
+
cmd.redirects.push({ op: t.op as RedirectOp, target: target.word });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
pos++;
|
|
244
|
+
const first = t.word[0];
|
|
245
|
+
const m = !cmd.words.length && first?.kind === 'text' && !first.quoted ? ASSIGNMENT.exec(first.value) : null;
|
|
246
|
+
if (m) {
|
|
247
|
+
const rest = first!.kind === 'text' ? first!.value.slice(m[0].length) : '';
|
|
248
|
+
cmd.assignments.push({ name: m[1]!, value: [...(rest ? [{ kind: 'text', value: rest, quoted: false } as Segment] : []), ...t.word.slice(1)] });
|
|
249
|
+
} else cmd.words.push(t.word);
|
|
250
|
+
}
|
|
251
|
+
return cmd;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const pipeline = (): Pipeline => {
|
|
255
|
+
let negate = false;
|
|
256
|
+
const t = peek();
|
|
257
|
+
if (t?.type === 'word' && t.raw === '!') {
|
|
258
|
+
negate = true;
|
|
259
|
+
pos++;
|
|
260
|
+
}
|
|
261
|
+
const commands = [command()];
|
|
262
|
+
while (isOp('|')) {
|
|
263
|
+
pos++;
|
|
264
|
+
while (isOp('\n')) pos++;
|
|
265
|
+
if (!peek()) throw new ShellSyntaxError('unexpected end of input after |', true);
|
|
266
|
+
commands.push(command());
|
|
267
|
+
}
|
|
268
|
+
for (const c of commands) if (!c.words.length && !c.assignments.length && !c.redirects.length) throw new ShellSyntaxError('syntax error near unexpected token `|\'');
|
|
269
|
+
return { negate, commands };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const script: Script = [];
|
|
273
|
+
while (pos < tokens.length) {
|
|
274
|
+
if (isOp(';') || isOp('\n')) {
|
|
275
|
+
pos++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const t = peek()!;
|
|
279
|
+
if (t.type === 'op' && !REDIRECTS.has(t.op)) throw new ShellSyntaxError(`syntax error near unexpected token \`${t.op}'`);
|
|
280
|
+
const list: AndOrList = { first: pipeline(), rest: [], background: false };
|
|
281
|
+
while (isOp('&&') || isOp('||')) {
|
|
282
|
+
const op = (tokens[pos] as { op: '&&' | '||' }).op;
|
|
283
|
+
pos++;
|
|
284
|
+
while (isOp('\n')) pos++;
|
|
285
|
+
if (!peek()) throw new ShellSyntaxError(`unexpected end of input after ${op}`, true);
|
|
286
|
+
list.rest.push({ op, pipeline: pipeline() });
|
|
287
|
+
}
|
|
288
|
+
if (isOp('&')) {
|
|
289
|
+
list.background = true;
|
|
290
|
+
pos++;
|
|
291
|
+
}
|
|
292
|
+
script.push(list);
|
|
293
|
+
}
|
|
294
|
+
return script;
|
|
295
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `jsh` program: `jsh -c "…"`, `jsh script.sh`, a script on stdin, or —
|
|
3
|
+
* attached to a terminal — the interactive shell. Interactively the shell owns
|
|
4
|
+
* the keyboard (raw mode) while it edits a line, and hands it to the running
|
|
5
|
+
* job otherwise: straight through to a terminal-attached child (which has its
|
|
6
|
+
* own line discipline), or cooked by the shell for everything else.
|
|
7
|
+
*/
|
|
8
|
+
import type { Program, ProgramContext } from '../system/types.js';
|
|
9
|
+
import { InputQueue, LineDiscipline } from '../system/terminal.js';
|
|
10
|
+
import { join } from '../paths.js';
|
|
11
|
+
import { binDirectories } from '../system/commands.js';
|
|
12
|
+
import { parse, ShellSyntaxError } from './parse.js';
|
|
13
|
+
import { Shell, type JobInput, type ShellCommand, type ShellIO } from './exec.js';
|
|
14
|
+
import { LineEditor } from './lineEditor.js';
|
|
15
|
+
import { BUILTINS, COREUTILS, SYSTEM_PROGRAMS } from './builtins.js';
|
|
16
|
+
import { npmCommand, npxCommand } from './npm.js';
|
|
17
|
+
import { glob } from './expand.js';
|
|
18
|
+
|
|
19
|
+
export const SHELL_COMMANDS: Record<string, ShellCommand> = { npm: npmCommand, npx: npxCommand };
|
|
20
|
+
|
|
21
|
+
function createShell(ctx: ProgramContext, positional: string[] = []): Shell {
|
|
22
|
+
return new Shell({
|
|
23
|
+
system: ctx.system,
|
|
24
|
+
cwd: ctx.cwd,
|
|
25
|
+
env: { ...ctx.env, SHELL: '/bin/jsh' },
|
|
26
|
+
positional,
|
|
27
|
+
terminalSize: () => (ctx.io.terminal ? { cols: ctx.io.terminal.cols, rows: ctx.io.terminal.rows } : undefined),
|
|
28
|
+
commands: SHELL_COMMANDS,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The stdin a non-interactive run hands its commands. On a terminal the shell
|
|
34
|
+
* takes raw input so a terminal-attached child (node, a REPL) gets keystrokes
|
|
35
|
+
* once — its own line discipline cooks them — while everything else gets lines
|
|
36
|
+
* cooked here, exactly once.
|
|
37
|
+
*/
|
|
38
|
+
function stdinJob(ctx: ProgramContext, controller?: AbortController): { input: JobInput; stop(): void } {
|
|
39
|
+
const queue = new InputQueue();
|
|
40
|
+
const terminal = ctx.io.terminal;
|
|
41
|
+
if (!terminal) {
|
|
42
|
+
const stop = ctx.io.onStdin((chunk) => queue.push(chunk));
|
|
43
|
+
return { input: { queue }, stop };
|
|
44
|
+
}
|
|
45
|
+
terminal.setRawMode(true);
|
|
46
|
+
let rawSink: ((data: string) => void) | undefined;
|
|
47
|
+
const discipline = new LineDiscipline({
|
|
48
|
+
echo: (t) => ctx.io.stdout(t),
|
|
49
|
+
deliver: (chunk) => queue.push(chunk),
|
|
50
|
+
signal: (name) => controller?.abort(name),
|
|
51
|
+
});
|
|
52
|
+
const stop = ctx.io.onStdin((chunk) => {
|
|
53
|
+
if (chunk === null) queue.push(null);
|
|
54
|
+
else if (rawSink) rawSink(chunk);
|
|
55
|
+
else discipline.input(chunk);
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
input: { queue, setRawSink: (sink) => void (rawSink = sink) },
|
|
59
|
+
stop() {
|
|
60
|
+
stop();
|
|
61
|
+
terminal.setRawMode(false);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run with a signal that both the program's kill and a cooked Ctrl-C abort. */
|
|
67
|
+
async function runOnce(ctx: ProgramContext, shell: Shell, source: string): Promise<number> {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const onAbort = () => controller.abort(ctx.signal.reason);
|
|
70
|
+
ctx.signal.addEventListener('abort', onAbort);
|
|
71
|
+
const { input, stop } = stdinJob(ctx, controller);
|
|
72
|
+
try {
|
|
73
|
+
return await shell.exec(source, text(ctx), input, controller.signal);
|
|
74
|
+
} finally {
|
|
75
|
+
stop();
|
|
76
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const text = (ctx: ProgramContext): ShellIO => ({
|
|
81
|
+
stdout: (t) => ctx.io.stdout(t),
|
|
82
|
+
stderr: (t) => ctx.io.stderr(t),
|
|
83
|
+
isTerminal: !!ctx.io.terminal,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function displayCwd(cwd: string, home: string | undefined): string {
|
|
87
|
+
if (home && (cwd === home || cwd.startsWith(home + '/'))) return '~' + cwd.slice(home.length);
|
|
88
|
+
return cwd;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function completer(shell: Shell) {
|
|
92
|
+
return (before: string, word: string): string[] => {
|
|
93
|
+
const isCommand = !/\S\s/.test(before.trimStart()) && !word.includes('/');
|
|
94
|
+
const fs = shell.system.fs;
|
|
95
|
+
const dirPart = word.includes('/') ? word.slice(0, word.lastIndexOf('/') + 1) : '';
|
|
96
|
+
const files = glob(fs, shell.cwd, `${dirPart}*`).filter((f) => f.slice(dirPart.length).startsWith(word.slice(dirPart.length)) || !dirPart)
|
|
97
|
+
.filter((f) => f.startsWith(word))
|
|
98
|
+
.map((f) => (fs.isDirectory(f.startsWith('/') ? f : join(shell.cwd, f)) ? f + '/' : f));
|
|
99
|
+
if (!isCommand) return files.sort();
|
|
100
|
+
const commands = new Set([...Object.keys(BUILTINS), ...Object.keys(COREUTILS), ...SYSTEM_PROGRAMS]);
|
|
101
|
+
for (const dir of binDirectories(shell.cwd)) for (const name of fs.readdir(dir)) commands.add(name);
|
|
102
|
+
return [...[...commands].filter((c) => c.startsWith(word)).sort(), ...files.filter((f) => f.endsWith('/'))];
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function interactive(ctx: ProgramContext): Promise<number> {
|
|
107
|
+
const terminal = ctx.io.terminal!;
|
|
108
|
+
const shell = createShell(ctx);
|
|
109
|
+
const io = text(ctx);
|
|
110
|
+
terminal.setRawMode(true);
|
|
111
|
+
const editor = new LineEditor({ write: (t) => ctx.io.stdout(t), history: shell.history, complete: completer(shell) });
|
|
112
|
+
const prompt = () => `\x1b[1;36m${displayCwd(shell.cwd, shell.env.HOME)}\x1b[0m \x1b[1;32m$\x1b[0m `;
|
|
113
|
+
|
|
114
|
+
let job: { controller: AbortController; input: JobInput; discipline: LineDiscipline; rawSink?: (data: string) => void } | undefined;
|
|
115
|
+
let pending = '';
|
|
116
|
+
let typeahead = '';
|
|
117
|
+
let finish!: (code: number) => void;
|
|
118
|
+
const done = new Promise<number>((resolve) => (finish = resolve));
|
|
119
|
+
|
|
120
|
+
const runLine = async (line: string) => {
|
|
121
|
+
const queue = new InputQueue();
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const discipline = new LineDiscipline({
|
|
124
|
+
echo: (t) => ctx.io.stdout(t),
|
|
125
|
+
deliver: (chunk) => queue.push(chunk),
|
|
126
|
+
signal: (name) => controller.abort(name),
|
|
127
|
+
});
|
|
128
|
+
const current: NonNullable<typeof job> = { controller, input: { queue }, discipline };
|
|
129
|
+
current.input.setRawSink = (sink) => void (current.rawSink = sink);
|
|
130
|
+
job = current;
|
|
131
|
+
try {
|
|
132
|
+
await shell.exec(line, io, current.input, controller.signal);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
ctx.io.stderr(`jsh: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
135
|
+
}
|
|
136
|
+
job = undefined;
|
|
137
|
+
if (shell.exitRequested !== undefined) return finish(shell.exitRequested);
|
|
138
|
+
editor.begin(prompt());
|
|
139
|
+
if (typeahead) {
|
|
140
|
+
const rest = typeahead;
|
|
141
|
+
typeahead = '';
|
|
142
|
+
onInput(rest);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const onInput = (data: string) => {
|
|
147
|
+
if (job) {
|
|
148
|
+
if (job.rawSink) job.rawSink(data);
|
|
149
|
+
else job.discipline.input(data);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let rest = data;
|
|
153
|
+
while (rest && !job) {
|
|
154
|
+
const { event, rest: remaining } = editor.feed(rest);
|
|
155
|
+
rest = remaining;
|
|
156
|
+
if (!event) break;
|
|
157
|
+
if (event.type === 'eof') {
|
|
158
|
+
ctx.io.stdout('exit\r\n');
|
|
159
|
+
return finish(shell.lastStatus);
|
|
160
|
+
}
|
|
161
|
+
if (event.type === 'interrupt') {
|
|
162
|
+
pending = '';
|
|
163
|
+
shell.lastStatus = 130;
|
|
164
|
+
editor.begin(prompt());
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const source = pending ? `${pending}\n${event.line}` : event.line;
|
|
168
|
+
try {
|
|
169
|
+
parse(source);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof ShellSyntaxError && error.incomplete) {
|
|
172
|
+
pending = source;
|
|
173
|
+
editor.begin('> ');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
pending = '';
|
|
178
|
+
if (!source.trim()) {
|
|
179
|
+
editor.begin(prompt());
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
typeahead = rest;
|
|
183
|
+
rest = '';
|
|
184
|
+
void runLine(source);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const stopInput = ctx.io.onStdin((chunk) => (chunk === null ? finish(shell.lastStatus) : onInput(chunk)));
|
|
189
|
+
const onAbort = () => {
|
|
190
|
+
job?.controller.abort('SIGHUP');
|
|
191
|
+
finish(129);
|
|
192
|
+
};
|
|
193
|
+
ctx.signal.addEventListener('abort', onAbort);
|
|
194
|
+
editor.begin(prompt());
|
|
195
|
+
const code = await done;
|
|
196
|
+
stopInput();
|
|
197
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
198
|
+
terminal.setRawMode(false);
|
|
199
|
+
return code;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const shellProgram: Program = async (ctx) => {
|
|
203
|
+
const args = ctx.args.filter((a) => a !== '-l' && a !== '--login' && a !== '-i');
|
|
204
|
+
if (args[0] === '-c') return runOnce(ctx, createShell(ctx, args.slice(2)), args[1] ?? '');
|
|
205
|
+
if (args[0]) {
|
|
206
|
+
const path = args[0].startsWith('/') ? args[0] : join(ctx.cwd, args[0]);
|
|
207
|
+
const source = ctx.system.fs.readText(path);
|
|
208
|
+
if (source === undefined) {
|
|
209
|
+
ctx.io.stderr(`jsh: ${args[0]}: No such file or directory\n`);
|
|
210
|
+
return 127;
|
|
211
|
+
}
|
|
212
|
+
return runOnce(ctx, createShell(ctx, args.slice(1)), source.replace(/^#!.*\n/, ''));
|
|
213
|
+
}
|
|
214
|
+
if (ctx.io.terminal) return interactive(ctx);
|
|
215
|
+
// No terminal, no script: read commands from stdin.
|
|
216
|
+
const { input, stop } = stdinJob(ctx);
|
|
217
|
+
const source = await input.queue.readAll();
|
|
218
|
+
stop();
|
|
219
|
+
const shell = createShell(ctx);
|
|
220
|
+
const empty = new InputQueue();
|
|
221
|
+
empty.push(null);
|
|
222
|
+
return shell.exec(source, text(ctx), { queue: empty }, ctx.signal);
|
|
223
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text coreutils: echo, printf, head, tail, grep, wc, env, clear, sleep,
|
|
3
|
+
* basename, dirname, true, false. Each reads named files or, without any,
|
|
4
|
+
* stdin — so they work at either end of a pipe.
|
|
5
|
+
*/
|
|
6
|
+
import { basename as baseOf, dirname as dirOf, join } from '../paths.js';
|
|
7
|
+
import { parseFlags, resolvePath, type Command, type CommandContext } from './context.js';
|
|
8
|
+
|
|
9
|
+
/** Contents of each operand file, or stdin when there are none. */
|
|
10
|
+
async function inputs(ctx: CommandContext, operands: string[], name: string): Promise<Array<{ label: string; text: string }> | number> {
|
|
11
|
+
if (!operands.length || (operands.length === 1 && operands[0] === '-')) return [{ label: '(standard input)', text: await ctx.io.stdin.readAll() }];
|
|
12
|
+
const out: Array<{ label: string; text: string }> = [];
|
|
13
|
+
for (const file of operands) {
|
|
14
|
+
const path = resolvePath(ctx, file);
|
|
15
|
+
if (!ctx.fs.isFile(path)) {
|
|
16
|
+
ctx.io.stderr(`${name}: ${file}: ${ctx.fs.isDirectory(path) ? 'Is a directory' : 'No such file or directory'}\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
out.push({ label: file, text: ctx.fs.readText(path) ?? '' });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines = (text: string) => {
|
|
25
|
+
const list = text.split('\n');
|
|
26
|
+
if (list[list.length - 1] === '') list.pop();
|
|
27
|
+
return list;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function unescape(text: string): string {
|
|
31
|
+
return text.replace(/\\(n|t|r|\\|e|a|0)/g, (_m, c: string) => ({ n: '\n', t: '\t', r: '\r', '\\': '\\', e: '\x1b', a: '\x07', 0: '\0' })[c] ?? c);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const echo: Command = async (ctx) => {
|
|
35
|
+
let args = ctx.args;
|
|
36
|
+
let newline = true;
|
|
37
|
+
let escapes = false;
|
|
38
|
+
while (args[0] && /^-[neE]+$/.test(args[0])) {
|
|
39
|
+
if (args[0].includes('n')) newline = false;
|
|
40
|
+
if (args[0].includes('e')) escapes = true;
|
|
41
|
+
args = args.slice(1);
|
|
42
|
+
}
|
|
43
|
+
const text = args.join(' ');
|
|
44
|
+
ctx.io.stdout((escapes ? unescape(text) : text) + (newline ? '\n' : ''));
|
|
45
|
+
return 0;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const printf: Command = async (ctx) => {
|
|
49
|
+
const [format = '', ...rest] = ctx.args;
|
|
50
|
+
let i = 0;
|
|
51
|
+
const out = unescape(format).replace(/%([sdif%])/g, (_m, spec: string) => {
|
|
52
|
+
if (spec === '%') return '%';
|
|
53
|
+
const arg = rest[i++] ?? '';
|
|
54
|
+
return spec === 's' ? arg : String(spec === 'f' ? parseFloat(arg) || 0 : parseInt(arg, 10) || 0);
|
|
55
|
+
});
|
|
56
|
+
ctx.io.stdout(out);
|
|
57
|
+
return 0;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function headTail(which: 'head' | 'tail'): Command {
|
|
61
|
+
return async (ctx) => {
|
|
62
|
+
const numeric = ctx.args.find((a) => /^-\d+$/.test(a));
|
|
63
|
+
const { values, operands } = parseFlags(ctx.args.filter((a) => a !== numeric), ['n']);
|
|
64
|
+
const count = numeric ? Number(numeric.slice(1)) : values.n !== undefined ? Number(values.n) : 10;
|
|
65
|
+
const files = await inputs(ctx, operands, which);
|
|
66
|
+
if (typeof files === 'number') return files;
|
|
67
|
+
files.forEach((file, index) => {
|
|
68
|
+
if (files.length > 1) ctx.io.stdout(`${index ? '\n' : ''}==> ${file.label} <==\n`);
|
|
69
|
+
const all = lines(file.text);
|
|
70
|
+
const picked = which === 'head' ? all.slice(0, count) : count ? all.slice(-count) : [];
|
|
71
|
+
if (picked.length) ctx.io.stdout(picked.join('\n') + '\n');
|
|
72
|
+
});
|
|
73
|
+
return 0;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export const head = headTail('head');
|
|
77
|
+
export const tail = headTail('tail');
|
|
78
|
+
|
|
79
|
+
export const grep: Command = async (ctx) => {
|
|
80
|
+
const { flags, operands } = parseFlags(ctx.args);
|
|
81
|
+
const [pattern, ...files] = operands;
|
|
82
|
+
if (pattern === undefined) {
|
|
83
|
+
ctx.io.stderr('Usage: grep [-ivnrclEF] PATTERN [FILE]...\n');
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
let re: RegExp;
|
|
87
|
+
try {
|
|
88
|
+
const source = flags.has('F') ? pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : flags.has('E') ? pattern : pattern.replace(/\\([|(){}+?])/g, '$1');
|
|
89
|
+
re = new RegExp(flags.has('w') ? `\\b(?:${source})\\b` : source, flags.has('i') ? 'i' : '');
|
|
90
|
+
} catch {
|
|
91
|
+
ctx.io.stderr(`grep: invalid pattern: ${pattern}\n`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
let targets = files;
|
|
95
|
+
if (flags.has('r') || flags.has('R')) {
|
|
96
|
+
targets = [];
|
|
97
|
+
const walk = (path: string, label: string) => {
|
|
98
|
+
if (ctx.fs.isFile(path)) targets.push(label);
|
|
99
|
+
else for (const name of ctx.fs.readdir(path)) if (name !== 'node_modules' && !name.startsWith('.')) walk(join(path, name), label === '.' ? name : `${label}/${name}`);
|
|
100
|
+
};
|
|
101
|
+
for (const f of files.length ? files : ['.']) walk(resolvePath(ctx, f), f);
|
|
102
|
+
}
|
|
103
|
+
const sources = await inputs(ctx, targets, 'grep');
|
|
104
|
+
if (typeof sources === 'number') return 2;
|
|
105
|
+
const prefix = sources.length > 1;
|
|
106
|
+
let matched = 0;
|
|
107
|
+
for (const { label, text } of sources) {
|
|
108
|
+
let count = 0;
|
|
109
|
+
lines(text).forEach((line, index) => {
|
|
110
|
+
if (re.test(line) === flags.has('v')) return;
|
|
111
|
+
count++;
|
|
112
|
+
matched++;
|
|
113
|
+
if (flags.has('c') || flags.has('l') || flags.has('q')) return;
|
|
114
|
+
ctx.io.stdout(`${prefix ? label + ':' : ''}${flags.has('n') ? index + 1 + ':' : ''}${line}\n`);
|
|
115
|
+
});
|
|
116
|
+
if (flags.has('c')) ctx.io.stdout(`${prefix ? label + ':' : ''}${count}\n`);
|
|
117
|
+
if (flags.has('l') && count) ctx.io.stdout(label + '\n');
|
|
118
|
+
}
|
|
119
|
+
return matched ? 0 : 1;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const wc: Command = async (ctx) => {
|
|
123
|
+
const { flags, operands } = parseFlags(ctx.args);
|
|
124
|
+
const files = await inputs(ctx, operands, 'wc');
|
|
125
|
+
if (typeof files === 'number') return files;
|
|
126
|
+
const only = ['l', 'w', 'c'].filter((f) => flags.has(f));
|
|
127
|
+
for (const { label, text } of files) {
|
|
128
|
+
const counts: Record<string, number> = { l: (text.match(/\n/g) ?? []).length, w: text.split(/\s+/).filter(Boolean).length, c: new TextEncoder().encode(text).length };
|
|
129
|
+
const shown = (only.length ? only : ['l', 'w', 'c']).map((f) => String(counts[f]).padStart(7)).join(' ');
|
|
130
|
+
ctx.io.stdout(`${shown}${operands.length ? ' ' + label : ''}\n`);
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const env: Command = async (ctx) => {
|
|
136
|
+
ctx.io.stdout(Object.entries(ctx.env).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
|
|
137
|
+
return 0;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const clear: Command = async (ctx) => {
|
|
141
|
+
ctx.io.stdout('\x1b[2J\x1b[3J\x1b[H');
|
|
142
|
+
return 0;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const sleep: Command = async (ctx) => {
|
|
146
|
+
const seconds = Number(ctx.args[0] ?? 0);
|
|
147
|
+
if (!Number.isFinite(seconds)) {
|
|
148
|
+
ctx.io.stderr(`sleep: invalid time interval '${ctx.args[0]}'\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
await new Promise<void>((resolve) => {
|
|
152
|
+
const timer = setTimeout(resolve, seconds * 1000);
|
|
153
|
+
ctx.signal.addEventListener('abort', () => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
resolve();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return ctx.signal.aborted ? 130 : 0;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const basename: Command = async (ctx) => {
|
|
162
|
+
const [path = '', suffix] = ctx.args;
|
|
163
|
+
let base = baseOf(path);
|
|
164
|
+
if (suffix && base.endsWith(suffix) && base !== suffix) base = base.slice(0, -suffix.length);
|
|
165
|
+
ctx.io.stdout(base + '\n');
|
|
166
|
+
return 0;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const dirname: Command = async (ctx) => {
|
|
170
|
+
const path = (ctx.args[0] ?? '.').replace(/(.)\/+$/, '$1');
|
|
171
|
+
const cut = path.lastIndexOf('/');
|
|
172
|
+
ctx.io.stdout((cut < 0 ? '.' : cut === 0 ? '/' : path.startsWith('/') ? dirOf(path) : path.slice(0, cut)) + '\n');
|
|
173
|
+
return 0;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const trueCommand: Command = async () => 0;
|
|
177
|
+
export const falseCommand: Command = async () => 1;
|