diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-29 12:53:41 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-29 12:53:41 +0100 |
| commit | d32c25c60daa0376c0d9d7c7b6ccf84628469b06 (patch) | |
| tree | 3aaf54762943461ddd03523697361cad3995abf1 /modules/ext | |
| parent | 07bd77ea9b893bce7f0f0c1be9a1470c71b6ec3a (diff) | |
feat(newos4): register `test` / `[` so chains decide locally instead of asking a model (v4.6.135)
`[ -f "/qt.txt" ] && echo exists` — the probe that closed the v4.6.132 batch — was not a shell test
at all: both commands were unregistered, so the line fell through to the AI fallback and a model was
asked whether a file exists. `_evalTest()` has implemented `-f -d -e -z -n ! = !=` for the if/while
parser since the script runner was written; only the command was missing.
* `test` and `[` registered in ShellModule. Success resolves silently; false rejects with a
`silent` error so `&&`/`||` skip it — POSIX prints nothing on exit 1.
* `[` requires the closing bracket (`[ : missing ']'`), and that message is NOT silent.
* `test` already names the on-demand regression-suite runner ("test auto" / "test list" in
help.html). A argv that does not look like a test expression (`-f`/`-d`/`-e`/`-z`/`-n`/`!`,
or `a = b` / `a != b`) is delegated to the handler registered before us, so `test auto`
still runs the suite and the two meanings cannot collide.
* terminal.dispatchCommand() now honours `err.silent` — without it a top-level `test -f /nope`
printed `Error in ShellModule.test:` (measured on v4.6.134, fixed in v4.6.135).
* help.html documents the bracket form next to the existing `test` row.
Three tags were minted minutes apart — v4.6.133 (commands), v4.6.134 (suite delegation),
v4.6.135 (silent top-level) — each a real cache-bust for a fix that superseded the previous build;
the net change is this commit.
Verification
node modules/test/test-shell-test-cmd.js -> 24/24 pass (RED against HEAD: "test command
registered" fails, "m.commands.test is not a function")
node modules/test/test-pipe-guard.js -> all seven flags true
node modules/test/test-parse-args.js -> 28/28 · daily job -> node tests 22/22 pass
newos4-maintain.py -> atlas 355 commands (was 354), modules-index 349, help 364/364
live v4.6.135, read from the terminal DOM:
[ -f /qt.txt ] && echo F135 -> F135
test -f /nope || echo M135 -> M135
[ -d /nope ] && echo BAD || echo N134-> N134 (false branch skipped, nothing printed)
test -> no output (suite not loaded; POSIX false)
test -f /nope -> no output
js "a b".length | wc -c -> 1 (v4.6.132 pipe fix still intact)
The AI fallback is no longer reachable for a bracketed file test.
Diffstat (limited to 'modules/ext')
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 44 | ||||
| -rw-r--r-- | modules/ext/shell.js | 44 |
2 files changed, 84 insertions, 4 deletions
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js index 0f2e1b9..53f1205 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -170,6 +170,46 @@ class ShellModule { this.commands['script'] = (args) => this._cmdScript(args); this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); + // v4.6.133: `test` / `[` were not registered, so `[ -f /x ] && echo yes` fell through to the + // AI fallback and asked a model whether a file exists. _evalTest() already implemented the + // operators (-f -d -e -z -n ! = !=) for the if/while parser; the command was simply missing. + // `test` already means "run the regression suite" in the on-demand test-suite module, so a + // non-expression argument list is delegated to whatever handler was registered before us. + const prevTest = this.commands['test']; + this.commands['test'] = (args) => this._cmdTest(args, 'test', prevTest); + this.commands['['] = (args) => this._cmdTest(args, '['); + } + + /** True when the argv looks like a POSIX test expression rather than a suite-runner subcommand. */ + _looksLikeTest(expr) { + if (!expr) return false; + if (/^(?:!(?:\s|$)|-[fderwxszn]\b)/.test(expr)) return true; // `-f x`, `! -d y` + if (/\s(?:=|==|!=)\s/.test(expr)) return true; // `a = b` + return false; + } + + /** POSIX-ish `test`/`[`: silent on success, throws a silent error on false so `&&`/`||` skip. */ + _cmdTest(args, name, fallback) { + let expr = (args || []).join(' ').trim(); + if (name === 'test' && !this._looksLikeTest(expr) && typeof fallback === 'function') { + return fallback(args); // `test`, `test auto`, `test list` -> the suite runner + } + if (name === '[') { + if (!expr.endsWith(']')) throw new Error("[ : missing ']'"); + expr = expr.slice(0, -1).trim(); + } + if (!expr) { // `test` with no expression: false, and no output + const e = new Error(''); e.silent = true; throw e; + } + if (!this._evalTest(expr)) { + const e = new Error(''); e.silent = true; throw e; // POSIX: exit 1 prints nothing + } + } + + /** Error printer shared by the shell paths: `silent` errors are control flow (a false test), not failures. */ + _printErr(term, e) { + if (!e || e.silent) return; + (term || this.terminal).print('Error: ' + (e.message || e), 'error'); } // ── Hook into terminal.executeCommand ── @@ -259,7 +299,7 @@ class ShellModule { await self.commands[cmd](args); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } else { @@ -267,7 +307,7 @@ class ShellModule { await this.security.checkAndRun(expandedCmd); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } diff --git a/modules/ext/shell.js b/modules/ext/shell.js index cb26484..ff07d92 100644 --- a/modules/ext/shell.js +++ b/modules/ext/shell.js @@ -34,6 +34,46 @@ class ShellModule { this.commands['script'] = (args) => this._cmdScript(args); this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); + // v4.6.133: `test` / `[` were not registered, so `[ -f /x ] && echo yes` fell through to the + // AI fallback and asked a model whether a file exists. _evalTest() already implemented the + // operators (-f -d -e -z -n ! = !=) for the if/while parser; the command was simply missing. + // `test` already means "run the regression suite" in the on-demand test-suite module, so a + // non-expression argument list is delegated to whatever handler was registered before us. + const prevTest = this.commands['test']; + this.commands['test'] = (args) => this._cmdTest(args, 'test', prevTest); + this.commands['['] = (args) => this._cmdTest(args, '['); + } + + /** True when the argv looks like a POSIX test expression rather than a suite-runner subcommand. */ + _looksLikeTest(expr) { + if (!expr) return false; + if (/^(?:!(?:\s|$)|-[fderwxszn]\b)/.test(expr)) return true; // `-f x`, `! -d y` + if (/\s(?:=|==|!=)\s/.test(expr)) return true; // `a = b` + return false; + } + + /** POSIX-ish `test`/`[`: silent on success, throws a silent error on false so `&&`/`||` skip. */ + _cmdTest(args, name, fallback) { + let expr = (args || []).join(' ').trim(); + if (name === 'test' && !this._looksLikeTest(expr) && typeof fallback === 'function') { + return fallback(args); // `test`, `test auto`, `test list` -> the suite runner + } + if (name === '[') { + if (!expr.endsWith(']')) throw new Error("[ : missing ']'"); + expr = expr.slice(0, -1).trim(); + } + if (!expr) { // `test` with no expression: false, and no output + const e = new Error(''); e.silent = true; throw e; + } + if (!this._evalTest(expr)) { + const e = new Error(''); e.silent = true; throw e; // POSIX: exit 1 prints nothing + } + } + + /** Error printer shared by the shell paths: `silent` errors are control flow (a false test), not failures. */ + _printErr(term, e) { + if (!e || e.silent) return; + (term || this.terminal).print('Error: ' + (e.message || e), 'error'); } // ── Hook into terminal.executeCommand ── @@ -123,7 +163,7 @@ class ShellModule { await self.commands[cmd](args); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } else { @@ -131,7 +171,7 @@ class ShellModule { await this.security.checkAndRun(expandedCmd); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } |