@@ -14,6 +14,9 @@ if (typeof SharedArrayBuffer === "undefined") {
1414}
1515
1616const WASM_URL = "/wasm/uutils.wasm" ;
17+ // grep ships as its own standalone WASM module (it is not part of the
18+ // coreutils multicall binary), loaded lazily alongside it.
19+ const GREP_WASM_URL = "/wasm/grep.wasm" ;
1720const XTERM_CSS = "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" ;
1821const XTERM_CSS_INTEGRITY = "sha384-tStR1zLfWgsiXCF3IgfB3lBa8KmBe/lG287CL9WCeKgQYcp1bjb4/+mwN6oti4Co" ;
1922const XTERM_JS = "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js" ;
@@ -48,6 +51,7 @@ const FALLBACK_COMMANDS = [
4851 "sha1sum" , "sha224sum" , "sha256sum" , "sha384sum" , "sha512sum" ,
4952 "shred" , "shuf" , "sleep" , "sum" , "tee" , "true" , "truncate" ,
5053 "uname" , "unexpand" , "uniq" , "unlink" , "vdir" , "wc" ,
54+ "grep" ,
5155] ;
5256const AVAILABLE_COMMANDS =
5357 ( typeof WASM_COMMANDS !== "undefined" && Array . isArray ( WASM_COMMANDS ) && WASM_COMMANDS . length > 0 )
@@ -63,6 +67,7 @@ const LOCALE_SHORTCUTS = {
6367} ;
6468
6569let wasmModule = null ;
70+ let grepModule = null ; // standalone grep WASM module (null if unavailable)
6671let wasiShim = null ;
6772let terminal = null ;
6873let inputBuffer = "" ;
@@ -111,35 +116,68 @@ async function loadWasiShim() {
111116 return wasiShim ;
112117}
113118
114- async function loadWasm ( ) {
115- if ( wasmModule ) return wasmModule ;
116- const response = await fetch ( WASM_URL ) ;
119+ /**
120+ * Fetch and compile a WASM module from the given URL.
121+ * Returns { module, size } where size is the downloaded byte length (0 if the
122+ * server didn't send a content-length header).
123+ */
124+ async function compileWasmModule ( url ) {
125+ const response = await fetch ( url ) ;
117126 if ( ! response . ok ) {
118127 throw new Error ( `Failed to fetch WASM binary: ${ response . status } ` ) ;
119128 }
120129 const contentLength = response . headers . get ( "content-length" ) ;
121- if ( contentLength ) wasmSize = parseInt ( contentLength , 10 ) ;
130+ const size = contentLength ? parseInt ( contentLength , 10 ) : 0 ;
122131 // compileStreaming requires application/wasm content-type; fall back if not set.
123132 // Clone the response so the fallback path can read the body without re-fetching.
124133 const cloned = response . clone ( ) ;
134+ let module ;
125135 try {
126136 if ( WebAssembly . compileStreaming ) {
127- wasmModule = await WebAssembly . compileStreaming ( response ) ;
137+ module = await WebAssembly . compileStreaming ( response ) ;
128138 } else {
129- wasmModule = await WebAssembly . compile ( await response . arrayBuffer ( ) ) ;
139+ module = await WebAssembly . compile ( await response . arrayBuffer ( ) ) ;
130140 }
131141 } catch ( e ) {
132142 // Some servers don't set proper MIME type, compile from the cloned response
133143 console . warn ( "WASM compileStreaming failed, falling back to arrayBuffer:" , e . message ) ;
134- wasmModule = await WebAssembly . compile ( await cloned . arrayBuffer ( ) ) ;
144+ module = await WebAssembly . compile ( await cloned . arrayBuffer ( ) ) ;
135145 }
146+ return { module, size } ;
147+ }
148+
149+ async function loadWasm ( ) {
150+ if ( wasmModule ) return wasmModule ;
151+ const { module, size } = await compileWasmModule ( WASM_URL ) ;
152+ wasmModule = module ;
153+ wasmSize = size ;
136154 return wasmModule ;
137155}
138156
157+ /**
158+ * Load the standalone grep WASM module. grep is optional: if the binary isn't
159+ * present (e.g. local dev without a CI build), this resolves to null and grep
160+ * commands report that they're unavailable rather than breaking the terminal.
161+ */
162+ async function loadGrepWasm ( ) {
163+ if ( grepModule ) return grepModule ;
164+ try {
165+ const { module, size } = await compileWasmModule ( GREP_WASM_URL ) ;
166+ grepModule = module ;
167+ wasmSize += size ;
168+ } catch ( e ) {
169+ console . warn ( "grep WASM unavailable:" , e . message ) ;
170+ grepModule = null ;
171+ }
172+ return grepModule ;
173+ }
174+
139175async function initWasm ( ) {
140176 if ( wasmReady ) return ;
141177 try {
142- await Promise . all ( [ loadWasiShim ( ) , loadWasm ( ) ] ) ;
178+ // grep is optional and loadGrepWasm swallows its own errors, so it never
179+ // blocks the coreutils module from becoming ready.
180+ await Promise . all ( [ loadWasiShim ( ) , loadWasm ( ) , loadGrepWasm ( ) ] ) ;
143181 wasmReady = true ;
144182 } catch ( e ) {
145183 // Will fall back to JS implementations
@@ -218,8 +256,9 @@ function lookupDir(path) {
218256 * Run a single uutils command via the WASM module using browser_wasi_shim.
219257 * Returns { stdout: string, stderr: string, exitCode: number }
220258 */
221- async function runCommand ( argv , stdinData = "" ) {
259+ async function runCommand ( argv , stdinData = "" , module = wasmModule ) {
222260 if ( ! wasmReady ) throw new Error ( "WASM not loaded" ) ;
261+ if ( ! module ) throw new Error ( "WASM module not loaded" ) ;
223262
224263 const encoder = new TextEncoder ( ) ;
225264
@@ -306,7 +345,7 @@ async function runCommand(argv, stdinData = "") {
306345
307346 let exitCode = 0 ;
308347 try {
309- const result = await WebAssembly . instantiate ( wasmModule , {
348+ const result = await WebAssembly . instantiate ( module , {
310349 wasi_snapshot_preview1 : wasi . wasiImport ,
311350 } ) ;
312351 // instantiate(Module) returns Instance; instantiate(Buffer) returns {instance}
@@ -453,6 +492,7 @@ async function executeCommandLine(line) {
453492 " echo '5 3 1 4 2' | fmt -w1 | sort -n\n" +
454493 " wc -l fruits.txt\n" +
455494 " seq 1 10 | factor\n" +
495+ " grep -i alice names.txt\n" +
456496 " basename /usr/local/bin/rustc\n" +
457497 " date\n" +
458498 " uname -a\n"
@@ -526,6 +566,13 @@ async function executeCommandLine(line) {
526566 return `uutils: command not found: ${ cmd } \nType 'help' for available commands.\n` ;
527567 }
528568
569+ // grep is a separate WASM module rather than part of the coreutils
570+ // multicall binary.
571+ const isGrep = cmd === "grep" ;
572+ if ( isGrep && ! grepModule ) {
573+ return "grep is not available in this build.\n" ;
574+ }
575+
529576 try {
530577 // Resolve relative paths using the virtual cwd
531578 const resolvedArgs = args . map ( ( arg , i ) => {
@@ -539,8 +586,21 @@ async function executeCommandLine(line) {
539586 if ( ! hasPathArg && cwd && [ "ls" , "dir" ] . includes ( cmd ) ) {
540587 resolvedArgs . push ( cwd ) ;
541588 }
542- const wasmArgs = [ "coreutils" , ...resolvedArgs ] ;
543- const result = await runCommand ( wasmArgs , stdinData ) ;
589+ // grep is invoked directly (argv[0] = "grep"); coreutils utilities go
590+ // through the multicall dispatcher (argv = ["coreutils", <util>, ...]).
591+ let dispatchArgs = resolvedArgs ;
592+ if ( isGrep ) {
593+ // browser_wasi_shim reports stdout as a TTY, so grep would emit GNU
594+ // match-highlight escape codes by default. That looks fine in the
595+ // terminal but corrupts piped/redirected output (e.g. `grep x | wc`),
596+ // so default to no color unless the user asks for it explicitly.
597+ const hasColorFlag = resolvedArgs . some ( a => a === "--color" || a . startsWith ( "--color=" ) ) ;
598+ dispatchArgs = hasColorFlag
599+ ? resolvedArgs
600+ : [ resolvedArgs [ 0 ] , "--color=never" , ...resolvedArgs . slice ( 1 ) ] ;
601+ }
602+ const wasmArgs = isGrep ? dispatchArgs : [ "coreutils" , ...resolvedArgs ] ;
603+ const result = await runCommand ( wasmArgs , stdinData , isGrep ? grepModule : wasmModule ) ;
544604 if ( result . stderr ) {
545605 return result . stderr + result . stdout ;
546606 }
@@ -864,6 +924,7 @@ window._uutilsTestInternals = {
864924 get locale ( ) { return currentLocale ; } ,
865925 set locale ( v ) { currentLocale = v ; } ,
866926 get wasmReady ( ) { return wasmReady ; } ,
927+ get grepReady ( ) { return grepModule !== null ; } ,
867928 initWasm,
868929 LOCALE_SHORTCUTS ,
869930 SAMPLE_FILES ,
0 commit comments