Changelog

Aggregated from each repo's CHANGELOG · fetched 2026-10-24 · latest 3 versions per product

  1. niubash 1.4.4

    Changed

    • WinuxCmd pin v1.1.19 → v1.1.20(#507,双 SHA-256 取自官方 release 资产 digest):裸 MSYS 盘根操作数(`ls /e`)跨 applet 面经共享 argv 边界解析(#1274);`env --file`(#1277)与 `csplit -f/--prefix`(#1278)同族折叠;每个 applet 的 `--help` 结尾带 unixwin.dev 门户链接(#1273);unity 构建在含正则元字符的检出路径下不再中止(#1281)。

    Fixed

    • `ping`/`netstat`/`tracert` were `command not found` (rubash#998/#1000): System32 ships these applets with UPPERCASE on-disk names (`PING.EXE`, `NETSTAT.EXE`, `TRACERT.EXE` — `HOSTNAME.EXE` too), and the engine's byte-exact stem contract (#799) rejected every lowercase invocation. The PATH fold arm now compares the stem ASCII-case-insensitively (byte-equal spellings still win; the resolved spelling is always the on-disk one) — parity with CMD, PowerShell and msys bash. The interactive crash on `${v^^}`/`${v:1:3}` input (niubash#503) ships in the same engine.
    • MSIX Store 版静默降级(#504/#505):旧 NSIS 安装残留的 `WINUXCMD_PATH`/`WINUXSH_ROOT` 环境变量会让 packaged 进程解析到旧安装树;package identity 下现按外来安装忽略。
    • Upgrades now reconcile the bundled package shims (`winuxcmd\usr\bin\gawk.exe`, `awk.exe`): `wpm links rebuild` cannot reach them on a bundled install because the product ships no `.wpm` state, so replacing `winuxcmd.exe` left those names on the previous image — the 1.4.1 stale `awk.exe`/`gawk.exe` residue of #468. The installer runs `niu --relink-bundled-shims --force` after the wpm rebuild, and shell-startup activation runs the same reconcile: missing or stale declared names are parked and relinked to the current `winuxcmd.exe`, extracted copies are upgraded to real hardlinks, and names owned

    Updates

    • Release carrying the rubash 1.4.3 engine (command-stem case fold — `ping`/`netstat`/`tracert` reachable again — plus the interactive `${v^^}` crash fix), the WinuxCmd v1.1.20 pin, and the MSIX foreign-env hardening.
  2. rubash 1.4.3

    新增

    • 仓库 logo(#988):青色 rotor-r 引擎标识。

    性能

    • splits_unquoted no-IFS 短路重落地(#995/#1002):#992 门因空展开折叠语义回退(无引号空展开把一个字段折叠为零、词消失),重落地版加入 `!expanded.is_empty()` 准入;splits_gate 4 万对抗输入 × 5 IFS 态差分网兜底,Linux 13/13 upstream 套件与基线逐字节一致。
    • assoc 交错访问固定成本(#914):`k[i]`/`k$i` 交错写读尾部的每操作固定开销削减;export/unset 交错循环二段身体削减(#984/#986/#920 后续)。
    • 信号提示事件每 8 个命令边界采样(#682):信号 hint event 从每命令一次降频。
    • `[ $VAR -op N ]` 整数比较快展开:测试面热路径。
    • 裸 `` 全局排序、mapfile 批量拆分/存储、数组元素 quote 单分配等 #607-#609/#661-#680 系列切片全部落定(详见下方各条)。

    修复

    • 命令词干大小写折叠(#998/#1000):System32 的盘上真名全大写(`PING.EXE`/`NETSTAT.EXE`/`TRACERT.EXE`/`HOSTNAME.EXE`),#799 的词干字节精确契约使小写 `ping`/`netstat`/`tracert` 全部 `command not found`(`PING` 大写反而能跑、`which`/`command -v` 两张脸不一致)。fold 臂改为 stem ASCII 大小写折叠(字节相等仍优先;无扩展名盘名保持字节精确;解析拼写恒报盘上真名)——与 CMD/PowerShell/msys 全生态一致;#799 的 harvest 误抬面(小写 `r` 抬 `R.exe`)按 Windows 生态惯例重新接受。tests/issue799 套件改写为新契约并新增 System32 大写盘名回归。
    • 行编辑器 `${v^^}`/`${v:1:3}` 崩溃(niubash#503,c143699c):下标 walker 里不可达的 `LEX_WASDOL` `${` 臂被以裸 `{` 语义消费,交互输入含上述序列触发 6 GiB 分配失败(MSIX 与 NSIS 1.4.1 双复现)。删除该臂。
    • cli_tests 与宿主 Winuxsh 安装面隔离(#996):测试 spawn 不再被宿主安装的环境变量污染。
  3. WinuxCmd 1.1.20

    Added

    • `--help` footer: every applet's `--help` output now ends with the
    • unixwin.dev portal link, so the documentation surface is reachable from
    • the binary itself (#1273). The README and the guides link the portal too
    • (#1271).

    Fixed

    • bare drive-root operands (`ls /e`, `cp /e ...`, every applet that
    • touches the filesystem outside the shared `native_path` boundary): a bare
    • MSYS drive root (`/e`, `/e/`) bypassed the drive-dialect recognizer and
    • decayed to a cwd-drive-relative path, so resolution depended on the
    • caller's drive. Glob expansion, digest open-failure probes, `mktemp` and
    • the classification probes of head/tail/sort/nl/fmt/fold/comm/join/paste/
    • cut and the *sum family now run on the resolved spelling; `df` keeps the
    • operand spelling for diagnostics. `sort -T` normalizes the dialect hint at
    • temp-dir construction and at chunk cleanup, so spill chunks no longer leak
    • onto the hinted drive root (#1230).
    • env --file/-f: the operand was opened with a raw `ifstream`, so MSYS
    • drive-dialect spellings (`/e/...`) decayed to a cwd-relative path and
    • missed the named file. `read_env_file` resolves the operand once through
    • `normalize_api_operand`; the directory-classification probe runs on the
    • native spelling while diagnostics keep the caller's text (#1275).
    • csplit -f/--prefix: same family — the output-operand prefix reached
    • `fopen` unfolded, so `/e/prefix-` created `prefix-00` on the cwd drive.
    • The prefix folds through the shared boundary before any creation
    • (#1276).
    • build: the unity build matched command sources with
    • `ABS_SOURCE_FILE MATCHES "${COMMANDS_DIR}.*"`, building the regex from a
    • literal path. On checkouts under paths containing regex metacharacters
    • (e.g. `D:\code\C++Files\...`) the pattern cannot compile and configure
    • aborts with "Regular expression ... cannot compile". The metacharacters
    • are escaped before the match (#1281).
  4. niubash 1.4.3

    Added

    • Launcher accepts `-EncodedCommand` and `--ec` spellings for the verbatim base64 handoff, with `v1:` rewrite-guard semantics preserved across all spellings (niubash#477).
    • `niu --help` and `--version` close with the unixwin portal lines (niubash#490).

    Fixed

    • Interactive multi-line groups no longer wedge the REPL: a stray `)` inside an open group now ends the gather with a single GNU-style diagnostic and rc 2, returning to the prompt (engine reader rubash#955 + host repl niubash#484; #482).
    • Bare drive designators (`D:`, `cd D:`) now converge on the engine dispatch across the host fast path, grouped route, and piped `niu -i` (rubash#953; #481, #483).
    • `unset` of an inherited environment name now clears the process-env copy (9-probe matrix realigned to GNU; #943 rework of #917).
    • Marker-zone literals in `-c` operands pass through verbatim — the respawn exclusion is double-escape only (rubash#952; #948).
    • `find_user_command`'s Windows listing negative cache now invalidates when candidate files appear, so newly installed commands are immediately visible (rubash#951).

    Updates

    • Release fixing the interactive gather wedge (P1), converging bare-drive behavior, hardening installer guards, and upgrading the vendored bash engine to patch 20. Carries WinuxCmd v1.1.19 (gawk BINMODE argv-prefix adapter) and the rubash 1.4.2 engine.

    Engine

    • Vendored bash upgraded from Bash-5.3 patch 15 to patch 20 (rubash#957): readline multibyte prompt redraw (17), long-prompt crash fix (18), high-byte identifier guard (19), SIGWINCH prompt recompute (20), macOS pipe sizing (16).
    • Marker-boundary divergences closed: E400-aware sentinel decode family for marker output (rubash#947, #840) and catalogue batches with an exhaustiveness gate (rubash#944, #746).
    • Environment: `export`/`unset` per-op cost reduction (rubash#943 env fast-path family).

    Security / hardening

    • Installer-surface guards: constant-fold coverage for `{syswow64}`/`{sysnative}`/`{autocf}`, `{sys}` name-boundary enforcement, and mutation vectors pinned by test (niubash#472).
    • The staged gawk pipeline gate now asserts raw output bytes — LF contract is enforced by the gate once WinuxCmd ships the BINMODE adapter (niubash#479).
  5. rubash 1.4.2

    性能

    • expand 面:多字面量快路径镜像加固、walker 入口链去重、procsub 扫描白名单跳过;vartable P1a 脏写收口。

    修复

    • env unbind 丢弃继承的进程 env 副本(#917 rework,#943):unbind 面残留清理。
    • read 对 fd-0 自 dup 走普通 fd-0 通道(#975)。
    • `${...}` IFS-mark 复制臂使用 cook 自身的 dolbrace span(#977):防逐臂特判回归。
    • Windows lane 补 `cargo test --lib`(#951),listing miss 以直查探针证明(#392 后续)。

    变更

    • BASH_COMPAT 广告位跟随 vendored PATCHLEVEL 20(#957/#895 链)。
  6. WinuxCmd 1.1.19

    Fixed

    • chgrp / chown: #1249 CR follow-ups (#1262). `//e/..`-class operands
    • (the POSIX two-slash rule keeps them rooted at `//e`, never `/`) now
    • report `cannot access '//e/..': No such file or directory` from both
    • commands - chown printed the Win32 invalid-name text where chgrp and
    • the MSYS oracle print ENOENT. `chgrp -R` propagates a failing
    • descendant through `process_directory` like chown's
    • `process_recursive` does, instead of still exiting 0. The operand
    • spelling convention is now documented in DOCS/gnu-coreutils: messages
    • quote the operand verbatim, so `///` prints as `///` where MSYS shows
    • `/` after its own argv-layer collapse (same refusal, display-only
    • divergence).
    • opt/ payload argv adapter (gawk CRLF): `usr\bin\<cmd>` shims and the
    • `winuxcmd <cmd>`/`<cmd>.exe` forwarding path now splice a fixed argv
    • prefix ahead of the forwarded arguments for commands that need one. The
    • only current entry is `awk`/`gawk` -> `-v BINMODE=3`: the bundled
    • ezwinports gawk is a text-mode mingw build whose CRT rewrote every `\n`
    • as `\r\n` on write and folded `\r\n` into `\n` on read, while every
    • winuxcmd applet does binary stdio (unixwin/niubash#467). That build
    • carries gawk's BINMODE feature but ignores the `BINMODE` environment
    • variable, so the command-line switch is the only lever; splicing it
    • first keeps a user's own `-v BINMODE=...` authoritative (later
    • assignments win). The `bin\<cmd>` PATH-entry link for adapted commands
    • now hardlinks a forwarder (`winuxcmd-shim.exe`, or `winuxcmd.exe` on an
    • installation that predates it) instead of the payload itself, and both
    • forwarders probe the installation root at the `bin\` depth as well as
    • the canonical `usr\bin\` one.
    • `winuxcmd <verb>` forwarding: `winuxcmd <name> args` forwarded the
    • verb itself as the payload's first argument, so `winuxcmd gawk '{print}'`
    • failed with "cannot open file '{print $1}'" and every `layout: shim`
    • payload reached through the verb form saw a corrupted argv. The verb
    • token is now skipped when the forwarded command line is rebuilt.
  7. niubash 1.4.2

    Changed

    • Release chain: the bundled rubash engine pin moves to `a83a09d32b4a4f953d29af0acc0e781b542206b8` (`1.4.1`) and the bundled WinuxCmd pin moves to `v1.1.18` (#466).

    Fixed

    • No more transient console windows when spawning external commands from a windowless or ConPTY host: the bundled WinuxCmd v1.1.18 `winuxcmd-shim` launcher suppresses orphan console creation when the parent has no visible console (#466, WinuxCmd#1282), rubash suppresses console windows on child process spawns from windowless parents (rubash#909), and niubash allocates at most one hidden console per session while adopting/suppressing orphan consoles across `fd::spawn_whitelisted`, `git_prompt`, completion helpers, plugin source git clones, and setup wizard probes (#462, #455).
    • `argv[0]`-marked login shells (`-bash`, `-niu`) preserve `shopt login_shell` as `on` across `reset_shell_options` (#465, rubash#911).
    • Unix platform shell discovery resolves the installed shell binary by platform binary name (`niu` on Unix vs `niu.exe` on Windows) (#463).
    • Golden-journey release audit fixes: ConPTY gallery pseudo-console window preservation (#455), sandbox hygiene and deterministic test gates (#442, #447, #449).
    • Release packaging: the product zip now carries `winuxcmd-shim.exe` beside `winuxcmd.exe` so upgrades replace a stale package-shim forwarder (the actual #466 fix payload) instead of leaving the 1.1.x buggy binary in place, and the x64 preinstall leg is fail-closed — a failed pin check, package install, or shim materialization stops the release instead of silently shipping a reduced bundle (#468). The gawk `unixwin2` artifact 404 that silently skipped the 1.4.1 gawk preinstall is fixed at the source (wpm-artifacts asset name).

    Updates

    • Release addressing P0 transient console window pop-ups across WinuxCmd shims, rubash child spawns, and niubash runtime helpers, plus the v1.4.1 golden-journey release audit fixes.
  8. WinuxCmd 1.1.18

    Fixed

    • winuxcmd-shim: `winuxcmd-shim.exe` previously gated child creation
    • flags on `GetConsoleMode(GetStdHandle(...))` and passed
    • `DETACHED_PROCESS` whenever stdio was redirected (`python - <<'EOF'`,
    • pipes, agent runs). That stripped the inherited console
    • (`GetConsoleCP() == 0` in the payload), causing every console-subsystem
    • grandchild (`python` -> `git.exe`, `node` -> child tools) to allocate a
    • fresh `conhost.exe` and flash a visible console window on the desktop
    • (unixwin/niubash#466). The shim now releases only a sole-owned *visible*
    • auto-allocated console (`IsWindowVisible` + `GetConsoleProcessList == 1`)
    • when stdio is non-console, keeps any inherited or hidden console
    • (`GetConsoleCP() != 0` -> `flags = 0`), and uses `CREATE_NO_WINDOW`
    • (never `DETACHED_PROCESS`) when `GetConsoleCP() == 0`.
    • mkfifo / mknod: FIFO/device-node path operands resolved against the
    • cwd drive (#1235, #1230 sweep). The raw `/e/p` spelling reached the
    • existence probe and `create_winux_fifo_w`, and Win32 roots a leading `/e`
    • on the CURRENT drive, so from a `C:` cwd `mkfifo /e/p` probed
    • `<cwd>:\e\p` (ENOENT for a real `E:\p`) and would have landed the marker
    • beside the cwd; `mknod /e p` from a `D:` cwd really created a `D:\e`
    • marker file. Operands now fold once through the shared
    • `make_api_path_operand` boundary before any filesystem call;
    • `/cygdrive/e`, `/mnt/e` and bare `/e` (which now reports "File exists"
    • like MSYS) share the fold, and messages keep the operand spelling as
    • typed.
    • chgrp / chown / chcon: `--reference` and target file operands
    • resolved against the cwd drive (#1241, same sweep). An unfolded
    • `/e/...` spelling made `GetFileAttributesW` /
    • `GetNamedSecurityInfoW` / the `WRITE_OWNER` open answer for
    • `<cwd>:\e\...`, so existing `E:` files were reported as missing from
    • any other cwd, `chgrp -R --preserve-root /e` missed the failsafe and
    • descended into `<cwd>:\e`, and chown/chcon globs enumerated the wrong
    • drive. Security lookups, attribute probes, the recursive walks and the
    • glob pattern all fold through the shared boundary first (children
    • re-fold from their display spelling, chmod `-R` precedent); error text
    • keeps the user's spelling. The preserve-root failsafe gained the same
    • fold, plus direct root matches for the spellings the operand layer
    • collapses onto `/`: all-separator forms (`//`, `///` - a verbatim
    • `GetFullPathNameW("//")` answers for an UNC-ish name, which let
    • `chown -R --preserve-root X //` bypass the check and really walk the
    • shell root) and lexically collapsing forms (`/e/..`, `/tmp/..`), the
    • latter with the `(same as '/')` suffix MSYS prints; `//e/..` keeps the
    • POSIX two-slash rule and resolves against `//e` like MSYS.
    • date: `date -r FILE` / `--reference=FILE` probed the operand the
    • same way - the raw spelling went straight to `GetFileAttributesExW`,
    • so `/e/Data/ref` and its `/cygdrive/e`, `/mnt/e` spellings reported
    • "failed to get modification time" from a foreign cwd for a file that
    • exists on the operand's drive (#1256, same sweep; the `-f DATEFILE`
    • open was fixed in #1239, this is that family's second read point in
    • date). The operand folds once through the shared
    • `normalize_api_operand` boundary before the attribute probe; the
    • failure message keeps quoting the operand exactly as typed, like MSYS.
  9. rubash 1.4.1

    修复

    • patsub 替换管道中的用户 PUA 码点 E400 转义(#843)。
    • 下标 ANSI-C 跨度的双引号态 + unset 操作数引号剥除(#847)。
    • pushd/popd 继承 cd 的 readonly bindpwd 裁决并指名真实的越界词(#844/#848)。
    • assoc 下标在 `((...))`/`let` 入口归一(#828)。
    • 复合命令自身的重定向失败诊断走 fd 表且只发一次(#826)。
    • `${var@P}` 命令替换状态括号、sudo 无 handler 路由钉死(#813 五项 CR 后续)。

    变更

    • crate 打包护栏:target 变体目录(如 target2-baseline,1.7GB 陈旧构建树)不得混入 cargo package(crates.io 上限 10MiB)。
    • `(A;B;)` 杂散闭括号族全模式钉到 GNU 5.3.0(#845)。
  10. oh-my-niu 1.0.1

    Changed

    • Released first-party `.winux` source packs for Git, Docker, kubectl, npm,
    • zoxide, direnv, dotenv, fzf, last-working-dir, and thefuck against the
    • Winuxsh source lifecycle host.
    • Added the framework entry point, directory-first official plugins,
    • `prompt-core`, and theme plugins for minimal, classic, pure, compact,
    • cyberpunk, forest, and ocean.
    • Kept keybindings and command-not-found as bridge plugins while their low-level
    • Reedline/provider call sites remain host-owned.
    • Kept old `packs/` manifests as compatibility and package-review metadata over
    • the directory plugin surface.
    • Added `tools/smoke_framework.winux` for runtime verification with an
    • installed Winuxsh binary.
    • Updated bundle metadata with `api = "winuxsh:[email protected]"` and
    • `min_winuxsh = "0.10.0"` so older Winuxsh hosts refuse this RC-first
    • source-pack bundle.
    • Added deterministic release packaging for `oh-my-winuxsh-1.0.1.zip` and the
    • matching `.sha256` checksum file.

    Compatibility

    • Requires Winuxsh 0.10.0 or newer for source plugin startup and lifecycle hook
    • loading through `.winux` files.
    • Keeps process and WASM plugin protocols unchanged; WASM provider/effect APIs
    • remain future host work.

    Release Checklist

    • Attach `oh-my-winuxsh-1.0.1.zip` and
    • `oh-my-winuxsh-1.0.1.zip.sha256` to the GitHub release.
    • Include this changelog entry and `docs/compatibility.md` in the release notes.
    • Run `python tools/validate_bundle.py` and
    • `python tools/package_bundle.py --check` before publishing.
    • Run `winuxsh tools/smoke_framework.winux .` before publishing framework
    • changes.
  11. oh-my-niu 1.0.0

    Added

    • Established the repository as the official Winuxsh plugin bundle rather than
    • a legacy `.winsh` script framework.
    • Added bundle metadata with `api = "winuxsh:[email protected]"` and
    • `min_winuxsh = "0.8.3"`.
    • Added first-party source packs for Git, Docker, kubectl, npm, zoxide,
    • direnv, dotenv, fzf, last-working-dir, and thefuck with bundle-local `.winux`
    • startup and lifecycle hook code.
    • Added first-party builtin/native packs for command-not-found, prompts, and
    • keybindings where host-owned provider/editor/prompt behavior still applies.
    • Added bundle-owned aliases, completions, prompt presets, keybinding metadata,
    • official theme assets, authoring templates, and public authoring documentation.
    • Added deterministic release packaging for `oh-my-winuxsh-1.0.0.zip` and the
    • matching `.sha256` checksum file.

    Compatibility

    • Requires Winuxsh 0.8.3 or newer for local bundle install, rollback, doctor,
    • review, process plugin, and Phase 8 WASM command execution support.
    • Uses pack manifest API `winuxsh:[email protected]`, process plugin protocol
    • `winuxsh:[email protected]`, and WASM plugin protocol
    • `winuxsh:[email protected]`.
    • Keeps source, process, and WASM execution visible through manifest
    • permissions because they can execute outside pure static asset loading.

    Release Checklist

    • Attach `oh-my-winuxsh-1.0.0.zip` and
    • `oh-my-winuxsh-1.0.0.zip.sha256` to the GitHub release.
    • Include this changelog entry and `docs/compatibility.md` in the release notes.
    • Run `python tools/validate_bundle.py` and
    • `python tools/package_bundle.py --check` before publishing.
  12. niu-git v2.56.0.1
  13. niu-git v2.55.0.5
  14. niu-git v2.55.0.4
  15. niubash Unreleased unreleased

    Added

    • `plugins::loader`: the generated-loader module — document format (banner, superseded-marker record, per-source sections byte-identical to the old block bodies), structural validation, atomic publish, the startup decision (opt-out switch → double-execution scan → load), the legacy-block scanner behind `migrate-rc`/doctor, and the one-time hint marker. Gate cases 359.4–359.9 and the 359.12 structure ratchet are pinned in `tests/rc_readonly_loader.rs`; the retired writers stay in `assets.rs` as an unwired rollback zone (design §4.3) with the ratchet asserting they are reachable from no verb.
    • Shell-face startup is a measured contract (`scripts/perf/shell-face-startup.py`):
    • execution-level `-c exit` timing for every installed face — the `niu.exe`
    • payload, the `winuxcmd\bin` / `winuxcmd\usr\bin` `bash.exe`/`sh.exe` links,
    • and the `C:\Program Files\Git\bin\bash.exe` convention slot — with a
    • verify-before-record rule (a face that cannot execute prints the failure
    • note, never a fast number), structural annotations (size, hardlink count,
    • same-file-as-payload), a `Measure-Command` cross-check mode, and a
    • deterministic `--check-links` gate: every declared face must be the same
    • file as the payload `niu.exe`. A measured-time-ratio gate was tried first
    • and rejected with data — shimmed faces measured 1.04–1.46x the payload
    • while healthy relinked faces measured 0.91–1.26x, overlapping intervals,
    • so no ratio separates the states without false kills (niubash#377 CR).
    • Measured on the 1.4.0
    • install (faces still the 243,200 B rubash forwarders): payload
    • `niu -c exit` 27.2 ms avg, faces 42.1–47.3 ms (+9.8 to +15.0 ms, one extra
    • process), the .NET slot 80.6 ms (+48 ms, two extra processes); after
    • `--relink-shells --force` on the same engine the faces measure equal to the
    • payload within noise (23.8–29.7 ms, same inode) with `$0`, `BASH_VERSION`,
    • POSIX mode and the `NIU_SHELL` contract verified through the links.
    • The installer sweeps retired shim shapes out of the PATH entry directory
    • (niubash#376 hygiene): `{app}\winuxcmd\bin` and `{app}\winuxcmd\usr\bin` are
    • cleared of `*.bak-*` / `*.old-*` / `*.niu-old-*` on install and uninstall —
    • the same dead-weight class as the existing `niu.exe.old-*` sweep, observed
    • live as `bash.exe.bak-1.4.0` / `sh.exe.old-shposix` (4 × 226,816 B on real
    • machines; `*.niu-old-*` covers a relinker crash between park and drop).
    • Live entry names are never matched. The PATH entry directory contract
    • (hardlinks only, `usr\bin` off `PATH`, declared entry set) is now documented
    • in `docs/src/installer.md`.
    • The installer's slot contract is pinned by tests, not by a comment
    • (niubash#398): the ruling that the install tree never claims the
    • `C:\Program Files\Git\bin\bash.exe` convention slot — the installer runs
    • `PrivilegesRequired=lowest`, cannot elevate, and the slot directory is
    • admin-walled — lived only in a GitHub comment until now.
    • `tests/installer_surface.rs` guards the surface statically: no absolute
    • foreign path or `Program Files` reference anywhere in `niubash.iss`, every
    • `[Files]`/`[InstallDelete]`/`[UninstallDelete]`/`[Icons]`/`[Run]` surface
    • confined to `{app}` and Inno-managed shell folders, `HKCU\Environment\Path`
    • the only registry value written, and the delete sweeps matched against the
    • sanctioned retired-shape globs only. `scripts/installer-slot-probe.py` is
    • the runtime leg: a pure user-directory sandbox replays the executable
    • installer steps (`--relink-shells`, app-tree teardown) across a full
    • install/uninstall cycle and asserts the decoy foreign slot — and the real
    • Git Bash slot, read-only — byte-identical at baseline, after install and
    • after uninstall, with no face name left outside the removed tree. The same
    • probe records the niubash#377 quantification: forwarder faces cost one
    • extra process per `-c` call (chain evidence: 2 processes below the probe,
    • min ~36 ms) and collapse to the payload itself as hardlinks (1 process,
    • min ~19 ms, same file as `niu.exe`).
    • The factory prompt's git segment runs the user's own git instead of the
    • in-process libgit2 dependency: one bounded
    • `git status --porcelain=v2 --branch` inside a repository, and no spawn at
    • all outside one (a few `stat`s, where `__git_ps1` forks anyway). Fidelity
    • is the reason to switch — sparse checkouts, linked worktrees, submodules and
    • `safe.directory` refusals are answered by the git the user actually runs
    • (libgit2 silently dropped the branch segment on old-owner trees once) — and
    • the cost is unchanged in practice: measured p50 26 ms / max 37 ms per prompt
    • inside a repository, i.e. the bare git call and nothing more, against Git
    • Bash's `__git_ps1` 100-300 ms (three to five forks). Isolation rules, one
    • per incident: never through a shell (a missing git is an OS error, not
    • `command not found` noise), stdout captured and stderr discarded (git
    • warnings never reach the screen), `--no-optional-locks` +
    • `GIT_OPTIONAL_LOCKS=0` (never takes `index.lock`), `-c core.fsmonitor=false`
    • (never starts an fsmonitor daemon from a prompt), a 2 s timeout with the
    • child killed, and no `CREATE_NO_WINDOW` (measured +28 ms per call: it makes
    • Windows allocate a hidden console for every prompt; a console child inherits
    • ours and shows no window). This also drops the `git2`/libgit2 dependency:
    • ~1.28 MB of the binary and 323 locked packages (libgit2-sys, url/idna/ICU,
    • cc, pkg-config, vcpkg, ...). Both niubash#145 startup invariants still pass
    • (`legacy_niu_plugins_rc_reaches_prompt`,
    • `legacy_framework_rc_reaches_prompt`): nothing is waited on before the first
    • paint, and the prompt-scoped child is reaped by the prompt itself. A
    • brand-new repository (unborn HEAD) now shows its branch, where libgit2's
    • `head()` errored and the segment vanished.

    Changed

    • The user rc is read-only to plugin management — the generated loader replaces the managed block (niubash#359 case A, 1.4.2). The control plane (`~/.niubash/plugins.toml` + `~/.niubash/sources/registry.toml`) now drives a derived activation plane: `niu plugin` verbs render and publish `~/.niubash/generated/plugins.niu` (deterministic, byte-stable when the manifest is unchanged, validate-then-atomic-publish so a failed generation never replaces the last good one and a concurrent reader never sees a missing file — the #304 invariant moves to the loader). The host sources that loader BEFORE the us
    • Double-execution guard for the migration window: the loader generation records the managed-block marker pairs it supersedes; at startup the host scans the rc read-only and keeps the loader OFF while any superseded block still executes (rather than run the plugin set twice), with a one-time hint naming `niu plugin migrate-rc`. Removing the old block by hand — the only removal there has ever been — turns the loader on. `niu doctor` reports the transition state read-only.
    • New verb `niu plugin migrate-rc [--diff]`: the read-only migration diagnostic. It scans the rc for legacy managed blocks (exact and hand-edited marker shapes, with line ranges and a begin-without-end malformed state reported, never fatal), publishes the candidate loader generation from the current manifest, records the superseded markers, and with `--diff` writes the suggested post-migration rc under `~/.niubash/candidates/`. It never modifies the rc — the removal is always the user's own edit.
    • Rollback switch: `NIU_PLUGIN_LOADER=off` (`off`/`false`/`0`/`no`) stops the host from loading the generated loader — plugins return to manual-source mode per the documented guidance. The switch only disables loading; it never writes anything, and unsetting it rolls forward.
    • Control-plane writers are atomic (design §4.4): `save_spec` (`plugins.toml`) and `write_source_registry` (`sources/registry.toml`) swap whole files through a same-dir temp + replace rename, so a concurrent reader never observes a truncate window.
    • clippy debt: `needless_borrow` cleared in `niubash-runtime` — first batch of niubash#450. `history.rs` passed `&mut state` — a `&mut HistoryState` already returned by `lock_mut()` — to `refresh_if_stale`/`sync_state`/`file_signature_lossy`, so each call built a `&mut &mut HistoryState` the compiler immediately dereferenced (11 sites across `reload_from_file`/`clear_memory`/`save`/`update`/`clear`/`delete`); `plugins/assets.rs` passed `&OMB_BLOCK` — a `&str` const — to `seed_legacy_block` in three rc fixtures. With the redundant borrows gone, the six `let mut state` bindings no longer need `mut
    • clippy debt: `niubash-runtime` cleared to zero — niubash#450 closes. The 105 unique warning sites left after the `needless_borrow` batch are resolved across ~40 categories: `question_mark` let-else rewrites in `shell.rs`, `is_some_and`/`is_none_or` for `map_or`, `contains`/`contains_key` for `iter().any`/`get().is_none()`, `strip_prefix` for manual prefix slicing, `clamp`/`is_multiple_of`/`is_ascii_uppercase` for hand-rolled checks, `matches!` for the keymap-target bool match, `#[derive(Default)]` for four hand-written impls, `sort_by_key`, `io::Error::other`, `c""` literals over `b"...\0"`, `
    • Windows path contract: the bundled WinuxCmd coreutils own the shell tree —
    • they receive the shell's POSIX spellings verbatim and resolve them in-child —
    • while every other child, native or a foreign POSIX runtime such as MSYS2 or
    • Cygwin, always receives native Windows paths, with no per-child probe and no
    • `MSYS_NO_PATHCONV`-style switch (unixwin/niubash#346). This rides the
    • WinuxCmd shell-tree layer, so the bundled WinuxCmd pin must be bumped in the
    • same release.
    • Windows releases bundle niu-git 2.55.0.5 (`scripts/release/preinstall.json`).
    • niu-git moves to `layout: flat`: the payload lands under `opt\niugit` and
    • `mingw64\bin\git.exe` is mapped straight onto `usr\bin\git.exe`, so the `git`
    • on PATH *is* the git binary — no winuxcmd dispatch hop, no launcher, no MSYS
    • runtime and no perl in the process tree. `git --version` drops from 72–83 ms
    • to ~14 ms (Git Bash: 45–103 ms) and `git status` to ~23 ms. Needs WinuxCmd
    • 1.1.9 or newer (the POSIX-unlink hardlink break, unixwin/WinuxCmd#1183);
    • the release pin already carries 1.1.10.

    Fixed

    • A DSR-silent terminal no longer kills the shell, and TAB completion
    • commits (unixwin/niubash#373, the #355 follow-up). Interactive startup
    • used to anchor the prompt with a `ESC[6n` cursor-position query and ABORT
    • the line editor when it timed out: on a terminal that never answers
    • (dumb terminals, `script(1)` recordings, some serial/tmux links) every
    • startup paid a ~2.6 s stall, printed
    • "niubash: line editor error: The cursor position could not be read within
    • a normal duration", and exited before painting any prompt. The editor
    • (reedline 0.50 -> 0.52) now treats the query as advisory — the prompt
    • paints without a reply and later cycles stay interactive; Windows/ConPTY
    • is unaffected either way (conhost withholds the client at session attach
    • until the terminal answers, a terminal-side contract below niu, pinned by
    • the new `tests/issue373.rs` harness). File completion now commits
    • like command completion: `cat uniqfi<TAB>` splices the completed word
    • into the line (the unique match commits outright, the ambiguous set
    • commits its common prefix and still shows the grid) instead of leaving
    • the suffix as a menu preview that Enter executed unexpanded — and Enter
    • submits the line as typed (GNU accept-line), dismissing the grid, where
    • reedline 0.52's menu would have consumed the first Enter as an
    • acceptance. The dismissal rides only the completion TAB window: a
    • Ctrl+R history search's accepting Enter stays the bare event reedline's
    • search handler implements (a whole `Multiple` batch is dropped there —
    • unconditionally wrapping it turned the search's Enter into a dead key). Measured TTP on the ConPTY harness is unchanged (niu 3.14 s
    • vs GNU bash 3.26 s median, transport-floor dominated; internal startup
    • ~70-140 ms — `scripts/perf/ttp-bench.py`).
    • dirname/basename: slash-shaped operands stay verbatim — `dirname /a/b`
    • reports `/a` instead of a drive spelling such as `A:`; the slash-drive
    • argument mask now covers the filesystem-blind string tools
    • (unixwin/niubash#346).
    • `(A;B);)` is a syntax error on every non-interactive route, like GNU (unixwin/niubash#431). `niu -c '(printf a; printf b);)'` ran the subshell and exited 0 — and so did script files, `--encoded-command`, and piped/redirected stdin — where GNU bash 5.3.0 rejects the shape in every mode (`syntax error near unexpected token ')'`, rc 2, subshell unexecuted; the earlier "msys GNU tolerates the tail" record came from the `winuxcmd/usr/bin/bash.exe` launcher shim echoing niu back — rubash#852). The host fast path now parses with the engine's stray-close guard armed (`ParseLoopOptions::stray_close_is_
    • Bundled gawk's CRLF output gets a byte-level release gate (unixwin/niubash#467). The `opt\gawk` payload is a text-mode mingw build — its CRT rewrote every `\n` as `\r\n` on write and folded `\r\n` into `\n` on read, while every other bundled applet does binary stdio, so `awk '{print}' > f` landed CRLF files and `ORS="\r\n"` doubled the CR. The fix ships in the bundled WinuxCmd forwarder (winuxcmd.exe / winuxcmd-shim.exe splice `-v BINMODE=3` ahead of the forwarded argv for `awk`/`gawk` — the build ignores the `BINMODE` environment variable, the `-v` assignment is its only lever, and a caller's
  16. rubash Unreleased unreleased

    新增

    • 脚本级函数级性能剖析器(#815):`niu --profile <script> [--profile-output <base>]`——解释器确定性插桩(非采样),在函数入口/出口(`execute_cmd.c:5200 execute_function` 家族)、简单命令分发点(`execute_cmd.c:624`,试探帧由真实分发自分类:函数/外部派发取消、其余确认为内建——无第二准入表)、外部 spawn 面、`( )` 子壳与 `$()` 命令替换独立栈帧(subst.c:7143)挂 Drop 守卫;return/exit/展开失败等中止形态经栈展开正确归因。运行时为 thread_local 调用树(self=elapsed−children,唯一 collapsed 路径聚合 count/self/total),归因三分离(内建/外部 spawn/子壳,self-time 切分面总和=100%)。输出文本调用树(self/children/calls 排序)+ collapsed-stack(flamegraph/speedscope 可渲染);报告只写文件(默认进程 temp 目录,绝不 stdout/stderr)——profile 开启时 stdout/stderr/rc 与关闭逐字节一致。关闭路径开销=每缝一次 relaxed AtomicBool lo

    性能

    • 字面单命令跳过词展开重建(#607,PR #665):每个简单命令在 `expand_command_words` 里付一次全量 `CommandNode` 重建(~1.34µs/次),裸 `:` 的展开恒为 identity。GNU 从不重建(`execute_cmd.c:4468` 跑存储的 SIMPLE_COM;`subst.c:13219` 无触发器文本无事可做)。白名单准入(单字面词、全部结构/重定向/赋值通道为空,`[`/`[[`/分发动词除外),共享 `preserves_word_metadata` 谓词(提取复用、永不复制),契约测试逐词锁定 fast==pipeline。`:` 循环 2.03x→1.77x GNU(~0.74µs/iter),`true` 1.86x→1.67x;`((1))` 路径按设计不动。门禁与 niubash 产品 A/B 零差异。
    • 大小写改写去分配+首字符短路(#608,PR #666):`apply_parameter_case_mod` 曾对每个字符跑一次 glob matcher 并 `to_string()` 堆分配;First 算子(`^ , ~`)按 GNU `subst.c case_transform` 只测首字符,空 pattern 即 `?`。First 算子现只测/改首字符后原文追加(栈缓冲零分配),空 `~~` 走直接翻转,主循环改栈缓冲。`${s^}` 500ch x20000 ~1.42s→~0.24s(~6x),`${s~~}` ~10%。33 例 GNU 差分电池 33/33;门禁与产品 A/B 零差异。
    • 标量赋值快 RHS 切片一(#609,PR #672):`t=$S` 循环 2.72x。`literal`/`$name`/`"$name"` 三形走白名单快路(字面按构造输出相等直接返回,dollar 拷贝一次借读 cell);dynamics、`set -u`、非名、IFS 标记值走通用路,绑定仍经 `apply_shell_assignment`。100k `t=$S` 1096ms→927ms(1.18x)。30 例差分电池 30/30;门禁与产品 A/B 零差异。
    • 标量绑定去双存储漫步切片二(#609,PR #678):`bind_marks_free_scalar`——一次 `attr_of` 探针放行无标记形,同值跳过双写,否则原地双写(镜像不变式成立);只读照常诊断(`variables.c:3258`)。net assign 2.03µs→1.33µs(−35%),GNU 1.25µs。电池 30/30;门禁与产品 A/B 零差异。
    • 算术循环机器(#607 切片三,PR #679):body 借指针重走(无逐 entry `Ast` 克隆/reader 包装),DEBUG 探针门控+体后重探,空表 reap 跳过。N=200k `:` 循环 −3.4%。6 契约测试;上游脚本逐字节一致;门禁与产品 A/B 零差异。
    • if 条件分发残留(#661,PR #673):空别名表时 raw 首词即展开首词,跳过逐命令 `expand_aliases`;单裸 `:`/`true` 条件跑无标记路径(影子函数/别名回退保 `set -e` 语义,附 4 回归测试)。50 命令 else 体 −26%;`ifcase` 55/55、`casearm` 29/30(仅预存 #17);门禁零差异(niu 50/49=已知负载抖动,隔离绿)。
    • case 静态臂零分配(#662,PR #669):`expand_case_pattern` 逐臂全管线+~5 String 层。默认拒绝分类器(`~` 经 GNU 探针确认为展开,掉队)命中则 `Cow::Borrowed`。每臂斜率 ~2.5µs→~1µs;`casearm` 29/30(仅预存 #17)、`ifcase` 55/55;门禁与产品 A/B 零差异。
    • mapfile 批量拆分+存储(#663,PR #670):100k 行 243ms vs GNU 52ms。拆分一次解码+字节域切分(`-t` 字节域剥离),无 `-O`/`-C` 时批量直写 dense 存储,跳过中间 index map。243ms→174ms(4.7x→3.3x)。26 探针矩阵 25/26(1 预存 NUL 分歧,新旧一致);门禁与产品 A/B 零差异。剩余存储侧是后片。
    • 裸 `` 全局排序(#664,PR #674):globstar 漫步深度优先,GNU `sh_sortglob` 全局重排。单 `` 调用既有 `globsort_matches` 全局重排(C/POSIX 字节序 else live collation)。repro 与 GNU 逐字节一致;glob 电池 33/34→34/34(关 glob-12 观察);门禁与产品 A/B 零差异。纯正确性片。
    • 数组元素 quote 单分配快路(#680 切片一,PR #686):`quote_array_value` 逐元素 2 marker 替换+解码扫描+`ansic_shouldquote`+4 链式替换。纯可打印 ASCII(`sh_double_quote` 转义集之外)直接渲染 `"value"` 单分配。seq100k mapfile ~181ms→~99ms(~1.8x)。矩阵 25/26(预存 NUL 分歧);门禁与产品 A/B 零差异。
    • mapfile 跳过整输入编解码往返(#680 切片二,PR #688):ASCII 无 carrier 字节输入解码/重编码皆恒等,直接切分(每段单分配)。seq1M −6%。4 契约测试(含 carrier 回退钉死 GNU 输出);门禁与产品 A/B 零差异。
    • glob 排序一次 locale 解析(#681 切片 b,PR #689):逐比较 3 env 查询提到逐排序一次;C 下字节序、非 C 走原比较器(结果恒等)。`LC_ALL=C` 300 文件循环 −22%,默认 locale 不变。契约测试;双 locale 电池 34/34;门禁与产品 A/B 零差异。
    • `export`/`readonly` 补上越界负下标 abort(#635 后续,PR #677):`export -a a=( [-2]=q )` 曾打印 AFTER rc 0。`-a/-A` 委托内嵌 `declare_builtin`(`setattr.def:240-268`),`arrayfunc.c:804-809` any_failed + DISCARD(`subst.c:11169`)。复用 #658 原语:`oob_word` 上浮、操作数循环遇 OOB 即停、Executor 层裸诊断+exit 1+#637 abort。13 例新测试;18 例 GNU 探针矩阵 18/18;门禁与产品 A/B 零差异。
    • 算术票据快路(#607 切片二,PR #671):identity 字母 `(( ))` 仍付全票据。双门(语法形只读+状态门:写日志空、标识符 unset/纯十进制)跳过 shopt/nounset/标记/上下文/同步。dense 探针 ~5%(~200ns/eval),headline 不变。6 契约测试;门禁与产品 A/B 零差异。
    • for-list 绑定补 dequote(#675,PR #683):`for p in "f*"` 绑了 `f`+CTLESC+`*`(长 3,全局 glob 死),只有双引号 for-list 路跳过解引用。绑定点统一走存储侧 dequote(`subst.c:4807`)。`casearm` 29/30→30/30,`ifcase` 55/55;门禁与产品 A/B 零差异。根因在 case 上游。
    • quoted 复合元素保留引用态(#377,PR #730):`declare -a d3=("$x")` 存了 glob 结果。NoMatch 臂推解引用重包形式,消费端 glob 解引用文本丢引用态。现引号内序列化 CTLESC 保护;`=` 臂解保护判下标。文件 10/12→11/12(剩预存);glob 34/34;门禁与产品 A/B 零差异。
    • fd_redirects 并行竞态(#667,PR #676):6 测试并行红、串行绿。`external_fd_copy_bin_dir` 返回固定路径,多测试互删助手目录。现 pid+ThreadId+计数器唯一目录。并行 10/19→19/19(3x),串行 19/19。纯测试改动。
    • 平台专属测试分流(#144/#363/#555,PR #692/#693/#694):`cmd /c` 流合并 10 测、`#!/usr/bin/env` 9 测、反斜杠字面量 11 行——在非对应平台上注定红。整文件/逐行 `cfg(windows)` + `$I363_PROBE`/PATH 解析,可移植行全留。纯测试改动。
    • 测试进程 CWD 隔离(#690,PR #691):`cd` 内建 chdir 测试进程(语义正确),但 1586 测共用单进程 CWD,一个 `cd` 毒死后面全部相对 `target/` 用例(~990 失败摊满所有 part)。逐测 RAII `CwdGuard`(panic 安全),1589 处机械插入,零语义改动。`executor_tests` 串行 321/1229→1540/10(剩 10 全是预存环境/抖动);门禁与产品 A/B 零差异。串行为限(CWD 进程全局)。
    • respawn 传输可写探针回退(PR #752):无 TMPDIR 且 `/tmp` 不可写(Android 有不可写的 `/tmp`)时临时文件建不出,回退内联 300KB `-c` 死于 E2BIG。存在性改可写性探针(pid+计数器),一次退到进程 temp dir、二次退 CWD。6000 函数后台任务恢复;无残留文件。
    • Windows-only tmpdir 测试分流(#328/#331,PR #750):`/var/tmp` 映射与 TMPDIR 自注入引擎臂都是 `cfg(windows)`,测试在外失败。`#[cfg(windows)]` 门。纯测试改动。
    • 真 Unix stty(#282,PR #732):非 Windows 是空桩(恒 rc 0)。现 `isatty(0)` 探针(非终端全形态诊断+rc 1),终端下左到右执行(`size` 经 TIOCGWINSZ,raw 系经 termios),Windows 路不动。文件 2/4→4/4;门禁与产品 A/B 零差异。
    • 交互 rcfile 只 source 一次(#312,PR #712):`--rcfile gate.sh -i` 跑两遍 gate.sh。`run_startup_files` 已 source 后各驱动又直调一次。用 `__RUBASH_SOURCED_RC` 去重(空 HOME/非交互扩展/login 原样保留),`#297` exit 语义不变。append 探针 2→1;文件 6/9→9/9;门禁与产品 A/B 零差异。
    • 真 uid/euid 身份,去掉 root 模拟(PR #698):非 root 主机报 `UID`/`EUID` 0、`\$` 渲染 `#`。`uid_value` 回退硬编码 `"0"`(开发基线跑在 root 下的模拟)。现取真 `getuid()`/`geteuid()`(GNU `variables.c:984`,继承值保留,Windows 保持 0)。4 个 prompt 测试曾钉作者账号、`reader` 默认 prompt 钉 `#`,现按宿主校准。三方探针与 GNU 一致;门禁与产品 A/B 零差异。
    • 关联数组元素写不再全量重渲染(#618,PR #650):`m[k]=v` 每次写曾做三次 O(keys) 操作(镜像字符串克隆、解析 memo 的 Vec 深拷贝、全量重渲染),填充数组是 O(n²),K=5000 时单次写比 GNU 慢 433 倍。现改为 O(1) 追加一段 `[k]=v`,预算触发规范渲染压缩重复段(均摊 O(1),串长不超 2 倍规范长度);读路径零改动(追加永不过期)。K=5000 单次写快 45 倍,与 GNU 差距 445x→9.9x,key 数涨 5000 倍单次写仅漂 1.14x。附带修好一个 O(n²) tokenizer(每段 collect 整个剩余输入,10001 段真解析 563ms→17.8ms 线性)。50 例 GNU 差分电池 50/50;全量 failure-set 门禁与 niubash 产品 A/B 零差异。
    • 索引数组覆盖写不再全量重渲染(#616,PR #652):`a[i]=v` 同理 O(n²),K=5000 时 674x。覆盖/稀疏跳跃同样 O(1) 追加(indexed 读路径天然折叠重复,BTreeMap last-wins,无需改解析/渲染;每次追加同步 typed cell;`declare -p` 等原文打印在有重复时才折叠重渲染)。K=5000 快 177 倍,与 GNU 差距 674x→3.8x。附带修好 `declare -a a=([5]=x [2]=y)` 种下 dense cell 导致后续元素写复刻的正确性 bug(另见 #651)。50 例差分电池 49/50(唯一分歧即 #651);门禁与产品 A/B 零差异。

    修复

    • `mark_quoted_ifs_from_raw` 模拟器补上 carrier 字节识别,per-char IFS 标记不再被 carrier 词静默跳过(#868):`mark_quoted_ifs_from_raw`(`src/executor/command_prepare.rs`,#743 的实现核心)用平行状态机重读 raw 词、模拟 quote removal,仅在模拟结果与 cooked 词逐字符相等时才落 `\x1c` per-char 标记,失配即静默 bail 退回粗粒度整拆/整不拆——而模拟器把 cook 产出的每个 markers.rs carrier(`$'...'` 解码输出、`$"..."` 体、逃逸引号、逃逸 IFS 空白/模式字符)都当普通数据字节,carrier 词逐字符必失配,最需要 per-char 规则的词恰恰被静默降级。现按 lexer 权威 walk(`lexer/quotes.rs` cursor 系列)逐臂复刻 carrier 变换:逃逸符在引号内外分别 cook 成 DATA_DOLLAR/DATA_BACKTICK/DATA_SQUOTE/DATA_BACKSLASH/DATA_DQUOTE、逃逸 IFS 空白 cook 成 ANSI_C_IFS_GUARD 对(自护形,不另记位)、逃逸模式字符 cook 成 CTLESC 对
    • `${var@P}` prompt 变换通道不再泄漏命令替换状态(#813):`PS1='x$(false)y'; p=${PS1@P}; echo $?` 曾打印 1——`@P` 算子通道(`apply_parameter_transform_value` 的 Prompt 臂)内嵌 `$( )` 的收割状态经替换状态 cell 被赋值边界当作替换退出规则发布。GNU 律(parse.y:6684-6697):`decode_prompt_string` 在 promptvars 遍历(`expand_prompt_string`,parse.y:6689)前后快照/恢复 `last_command_exit_value` 与 `last_command_subst_status`(subst.c string_transform 'P' 走同一 `decode_prompt_string`,`${var@P}` 同受覆盖);#604 修的是交互渲染通道(`expand_prompt_string_mut` 快照恢复 rc/PIPESTATUS),本修复把同一律落到共享 prompt 遍历引擎(`expand_embedded_parameters_double_quoted` 快照/恢复替换状态 cell,含管道 `-i` 渲染通道)与 mut 渲染通道的 cell 残留。恢复
    • `set -T`(functrace)下 DEBUG trap 透传进 `$()` 命令替换(#825):`set -T; trap 'echo D:$BASH_COMMAND' DEBUG; x=$(true)` 曾打印 `got=`——`$(true)`/`$(false)`/`$( :)` 的 trivial 体状态快捷路(`expand_command_substitution_inner`,command_substitution.rs)排在 functrace+DEBUG 取消资格门之上,硬编码空输出短路从未进入替换子壳,GNU trap.c:1466-1474(`reset_or_restore_signal_handlers` 仅在 `function_trace_mode == 0` 时清除 DEBUG/RETURN)要求的继承触发完全丢失。GNU subst.c:7143 `command_substitute` 把每个体交给 `parse_and_execute`,execute_cmd.c:4648-4649 对 `true`/`false`/`:` 照发头行与 DEBUG。修复:两快捷路移到门之下(与 xtrace 取消资格同门同型),门命中即走真实 parser/executor。WSL GNU 5.3.0(1) stdin oracle 8 形态逐字节
    • 复合命令自身重定向失败诊断走 fd 表、单次发送(#826):`{ :; } 2>err2.txt > /nonexistent_dir/f` 曾把诊断打印两次到真实 stderr 且 err2.txt 根本没建——brace 包装层按字段序(`redirect_out` 先于 `redirect_err`)应用重定向,fd-1 open 在 fd-2 绑定前失败;错误被 ast_exec 分发的 IoError 臂吞掉后不 `continue`,同一命令经通用路再分发、再失败、再打印(面 1/2 双打印 + 捕获丢失)。`( { :; } > /nonexistent_dir/f ) 2>err.txt` 则"捕获 + 泄漏"并存:包装层 raw `eprintln!` 绕过子壳已建立的 fd 2 绑定(面 3)。GNU(redir.c:767-955 `do_redirection_internal` 按解析序左到右;redir.c:135 `redirection_error` 经当时生效的 fd 2 报告):诊断一次、落在重定向目标里。修复:brace 四个 stdio 臂改解析序处理(位置取自 `command.redirects`),`2>`/`2>>` 文件目标记为失败报告目的地(不做常驻 fd_table[2] 绑定——常驻绑定会被 walker 下游的 `1>&2
    • alias 同读守卫改单调读取生成身份,跨 read 单元不再误抑制(#817):执行级 alias 同行可见性守卫拿定义时的物理行号当身份(`__RUBASH_ALIAS_LINE_<name>` vs `__RUBASH_CURRENT_LINE`,`arithmetic_aliases.rs:867-882`),而嵌套读单元会重排行号——source 的文件从 1 重新计数、eval 串按 `caller_line+i-1` 续号(`trap_exec.rs` execute_eval_source)——定义行与外层后续物理行相撞时合法使用被误报 `command not found` rc=127,GNU 展开(规则是同读不是同行,parse.y 读时展开)。修复:定义改记 `(读取代, 行)` 二元组(`__RUBASH_ALIAS_GEN_<name>`,`ShellState::read_generation` + 单调高水位 `read_generation_ceiling`,兄弟单元永不共享身份),生成边界正是现有 `CURRENT_LINE` 保存/恢复接合点——source 在 `builtins/source/execution.rs` 的 evalfile.c:253-257 push_stream 处 push、run_unwind_frame(eval
    • `set -E` ERR-trap 嵌套上下文语义:折叠压制、扁平区收口 fire、return 归因(#720,PR #831):三面均以 WSL GNU 5.3.0(1)(stdin oracle)逐字节钉死。① 嵌套子壳过火:`( (false) )` 曾 fire 3 次,GNU 2 次——execute_cmd.c:1799-1806 把整体为单个子壳命令的子壳体标 `CMD_NO_FORK`,execute_cmd.c:662-663 原位执行(无 make_child/wait_for),中间帧从不观察失败、其 :769 `run_error_trap` 永不运行;现以 `subshell_fork_collapse` 旗标圈住该形体(node 形 + `f() ( )` 扁平形两路),聚合点与 `set -e` 早转臂在其下跳过,非折叠中间层(`( (false); false )`、`( { (false); } )`)照常 fire。② 扁平函数体欠火:`f() ( false ); f` 曾 fire 2 次,GNU 3 次——子壳完成态的 ERR 裁定属于 wait 帧(execute_cmd.c:756-778,无 `&&/||` 豁免;`$LINENO`=子壳 `)` 行 execute_cmd.c:696;`BASH_COMMAND`=区文本,:703
    • 活动控制台 `read` 三面:`-t` 门饿死、deadline 行读被 conhost 预煮劈半、onechar 缺 ICRNL(#806,PR 于 `fix/806-read-conpty`):① `wait_readable`(`src/fd/windows_impl.rs`)只 peek 队头 1 条记录并要求 key-down——行编辑提交 Enter 的 key-RELEASE 卡在队头时,其后可消费的 key-down 永远不被看见,`read -t 2 -n1` 烧满 deadline 返 142 变量空(GNU select 同形 0.2s 取到键,WSL 5.3.0 oracle)。现 peek 16 条一批,批内任一"带字符的 key-down"(ReadFile 能吐出的)即 Ready,peek 不消费次序不动(niubash#280 的"空队列永不 Ready"保护原样保留)。② console 上的 deadline 读改走 one-char 路(`read.def:570-590` ttfd_onechar;read.def:659 每字节 check_read_timeout;GNU 的 zread 从不预煮 tty 行,read.def:736):cooked 行纪律与 raw 队列是两个缓冲,首次 cooked `ReadFile(1)` 把
    • tilde 与 cd/pushd/popd 共用同一 $HOME 基准:GNU 缺失/空值之分(#754):path model 家族——`HOME=` 置空后 `cd ~` 曾静默落进 USERPROFILE(rc 0),同脚本裸 `cd` 却报 "HOME not set";`$(cd && pwd)` 快路对置空 HOME 走 tilde 回退输出 USERPROFILE;`pushd ~` 空展开是无声成功、`pushd -n ~` 把 cwd 规范化后入栈(GNU 推原词,栈为 `<cwd> ""`)、`popd ~` 空词误报 invalid argument。GNU 规范(`lib/tilde/tilde.c:344-356` tilde_expand_word):`~`/`~/` 恒按展开时点的活 cell `sh_get_env_value("HOME")` 粘接(`glue_prefix_and_suffix` tilde.c:304-317,空值是真值——`HOME=; cd ~` 即 `cd ""` 报 "null directory",cd.def:331-336),仅变量缺失才落到回退链(_WIN32 APPDATA tilde.c:349-350;passwd `sh_get_home_dir` variables.c:736,getpwuid 固定于
    • PROMPT_COMMAND 执行后 `$_` 按 GNU 括回(#797):bash-preexec 类资产 source 后,DEBUG trap 里的 `preexec "$_"` 拿到的是 PC 内部最后的命令词(`bp_install`/`bp_precmd_invoke`)而非用户上一条命令的尾词。GNU `execute_variable_command` 在 PC 的 parse_and_execute 外括 `save_lastarg`/`bind_lastarg`(`parse.y:3017`、`3022-3023`;`save_parser_state` 不携带 lastarg,`7248/:7250` 只管 `$?`/PIPESTATUS);exit 跳转路径跳过恢复,与 `$?` 恢复同一规则。`bind_lastarg(NULL)` 绑空串(`execute_cmd.c:4192-4193`)。piped `-i` 最小 bash-preexec 形与 wezterm 全资产 transcript 与 WSL GNU 5.3.0 逐字节一致;回归 `tests/issue797_bash_preexec_debug_trap_regressions.rs`。
    • source 的 DEBUG trap 作用域改表驱动(#797):source 内安装的 DEBUG trap 曾在本次 source 余下命令上全部哑火(`-c`/脚本/交互三路同病)。GNU `source_builtin` 只快照+摘除入口 trap(`source.def:209-217` `restore_default_signal`),文件内新装的是活表状态照常触发(`trap.c:750-756`),unwind-protect `trap_if_untrapped`(`trap.c:737-743`)仅在退出时 DEBUG 无 trap 才归还入口 trap,且排在 RETURN trap(`evalfile.c:395`)之后。旧的 duration flag 对"入口本无 trap + 文件内新装"过宽。ignore 边界(CR R1/R2/R3):`trap '' DEBUG`(IGNORE,trap.c:900 同置 SIG_TRAPPED)与 `trap - DEBUG` 按 trap 表成员资格(`__RUBASH_TRAPS`)区分而非 action 串内容——入口 ignore 不快照不摘除(`TRAP_STRING` 对 ignored 返回 NULL,trap.h:57-59),文件内 ignore 令 `trap_if_untrapped`
    • #798 判定面钉死:source 中 `exit 1` 语义与 spawn 失败 `$?` 与 GNU 等价:LibreSSL testenc.sh 沙箱形态下 `cat openssl.cnf >p` 先截断后失败 → `cmp` 比的是两个空文件 → `exit 1` 本就不可达,47-cipher 循环跑穿是正确行为(WSL GNU 5.3.0 同样整循环,stdout 100 行/stderr 201 行 rc 0 逐字节一致;同机 rubash 4.5s < msys bash 5.2 5.3s,CI HANG 是 2 核 runner 8 并发下 ~400 次 spawn 的体量问题)。真实失败形态(cmp 差)首个失败即整体 rc 1 退出、交互会话同终止。回归 `tests/issue798_testenc_exit_in_source_regressions.rs`(hermetic:cmp/rm 用函数替身,钉 spawn-fail 127、空重定向目标、循环+if 形的 exit 终止)。
    • 算术下标补上 `set -u` 检查(写路径)(#639,PR #654):`set -u; a[k]=v` 曾静默存储 rc 0。GNU 所有下标经 `evalexp → expr_streval` 统一设卡(`expr.c:1190-1216`);写路径求值器补上与兄弟函数相同的 prologue,五处下游诊断在 flag 下静默, abort 走 FORCE_EOF 通道(`-c` 下 127、脚本 1),另用独立 cell 区分 DISCARD 以免状态错位。25 例差分电池 10/25→25/25;回归文件 `tests/issue639_nounset_subscript.rs`。
    • 复合赋值 `[@]`/`[*]` 报整词拒绝而非算术错(#636,PR #656):`a+=( [@]=q )` 曾报 `@: arithmetic syntax error`,GNU 在算术求值前整词拒绝(`arrayfunc.c:786-792`,`cannot assign to non-numeric index`)。两处 compound 臂加精确匹配 guard(不 abort,走 GNU break 语义:保留部分存储、eval 可包含)。15 例差分电池 4/15→14/15(剩 declare-abort 例归 #635);回归文件 `tests/issue636_atstar_compound.rs`。
    • `eval` 包含赋值 DISCARD 跳转(#637,PR #657):`eval 'a[-2]=q'` 曾结束整个脚本。GNU 在 eval 边界捕获 DISCARD(`evalstring.c:365,422-442`),eval 返回失败、调用者继续。新增 bad-subscript class marker,eval 边界精确吸收(算术/nounset 照常逃逸);eval 内按行继续,返回末行状态。20 例差分电池 9/20→20/20;回归文件 `tests/issue637_eval_discard.rs`。
    • declare/local 越界负下标 abort 而非静默丢弃(#635,PR #658):`declare -a a=( [-2]=q )` 曾打印 AFTER rc 0。builtin 解析器区分无头/越界,扫描中断并保留部分存储,报错词经 out-param 上浮到 Executor 层做 fatal 诊断+abort(复用 #637 原语实现 eval 包含)。20 例差分电池 6/20→20/20;回归文件 `tests/issue635_builtin_oob.rs`。`export`/`readonly` 的同类 abort threading 记作后续。
    • 函数定义 stdin 优先于调用点 stdin(#655,PR #684):#653 把优先级翻转了(`f() { cat; } < deffile; f < callfile` 打印 call)。GNU 调用点在外、定义在内(`execute_cmd.c:5205`),定义装了活 fd 0 就赢。`part_054` 14/14;门禁与产品 A/B 零差异。
    • 复合赋值字段分裂传输保留空字段(#298,PR #762):`IFS=.; A=($1)`(`a.b..b`)存了 3 个元素(`b" "`),GNU 存 4 个。`compound_field_split_transport` 对空字段发合成 tag+空格,重分只在空白 IFS 下认;现空体等下个定位 gap,用真实分隔符按序分发,空白 run 仍坍缩一边。`a.b..b`/`a::b`/`:a:` 全对 GNU;298 文件 12/12;GitHub CI 全绿(本地 master 腿 diff 因设备击杀循环未竟,见 PR 备注)。
    • issue358 探针目录沙盒化(#358,PR #766):`mkdir -p /tmp` 在 Android 失败→`cd`失败→`touch zzq` 落 repo 根(测试靠自身污染通过)。temp_dir pid 唯一目录,用后删除。14/14,无残留。纯测试改动(#763/#765 同类)。
    • fd 偏移测试经 PATH 解析外部 true(#655 后续,PR #687):硬编码 `/usr/bin/true` 在 Android(无 /usr/bin)上 127。测试时经 PATH 解析(绝对路径绕过 builtin 仍是外部 spawn),缺席则跳过。双测 Android 绿。纯测试改动。
    • prompt 渲染不再破坏读者的 `$?`/PIPESTATUS(#604):交互驱动(niu 原生控制台通道,`expand_prompt_string_mut`)渲染 `PS1`/`PS2`/`PS0` 时,内嵌 `$(false)` 等命令替换的退出状态此前经赋值展开引擎的替换边界写入 `exit_code`,下一个命令的 `$?` 读到 1;GNU 的 prompt 渲染是读侧操作,从不动读者的状态(WSL GNU Bash 5.3.0 piped-`-i` 探针:`PS1='$(false)'` 渲染后 `echo $?` 仍打印渲染前的值)。修复:渲染通道快照并在收尾恢复 `$?` 与 PIPESTATUS——赋值 RHS 通道(`v=$(false)` → rc=1 的 POSIX 赋值退出规则)不受影响。附带 #604 的通道 parity 回归与相对性能上限测试(PS1 渲染路径 vs PROMPT_COMMAND 路径的中位耗时比值上限),并按 profile 数据澄清该 issue 的主刀:四主题 rr 成本为外部 spawn 数 × Windows spawn 地板(每次渲染 4-6 次 git spawn;裸 `std::process::Command` 对同一 git.exe 需 208.9ms,rubash 会话内 217.6ms),两条引擎通道执行

    变更

    • wrapper 重spawn 的 argv 握手旗标按消费者在位门控(#873 后续):`--internal-bg-wrapper=<nonce>` 是 niu CLI 专属旗标,此前 wrapper 重spawn 无条件附加——嵌入式宿主(niubash 的 niu 启动器)与任何不经 `consume_background_wrapper_handshake` 的 CLI 会把它当未知选项拒收(`bash: --internal-bg-wrapper=<nonce>: invalid option` + usage),后台命令 spawn 即死(niubash master CI background_spawn_args 两红)。现旗标仅当本进程 CLI 在启动时调用过 consume(`background_wrapper_flag_consumer_present()`,rubash main.rs 每次启动必调)才附加;非消费者宿主得到 env-nonce-only 重spawn——env 键对非消费者惰性(不被读、消费者启动即清洗),单通道永不 arm(#866 契约不变)。rubash 本体行为逐 spawn 等价(消费者恒在位);宿主采纳 consume 即恢复完整双通道握手与 #737 cascade。
    • Windows 无宿主 sudo 回落 PATH 执行(#810 行为变化披露,#813 补记):`sudo cmd` 在 Windows 且嵌入宿主未注册 elevation handler 时,曾一律由 host builtin 应答 "elevation handler is not configured"(rc 1),PATH 从不被咨询;自 #810 起该名字按外部命令走 PATH 解析——有 sudo.exe 的机器(Win11 System32)真实提升执行,没有的报 command-not-found rc 127,`builtin sudo` 无 handler 时报 `builtin: sudo: not a shell builtin`(rc 1)。introspection 契约不变:`command -v`/`type -a`/`enable -a`/`compgen -b` 依旧把 sudo 报告为外部命令(docs/builtins.md §3 隐藏 builtin 条款)。方向与 GNU 更近(GNU 无 sudo builtin)。#813 以两条 windows-only 测试钉死两臂执行路由(假 sudo PATH 实跑、`builtin sudo` rc 1)。

    构建

    • 发行来源戳记补 dirty 工作树摘要(#827 建议 3):`--version` 的 `build:` 指纹此前只铸 `git rev-parse --short HEAD`——哈希诚实但不担保内容,v1.4.0 事故正是"banner 报某 commit、二进制实际编译了该 commit 加未提交改动"(#488 家族盲区:哈希可见、工作树内容不可验证)。现携带未提交改动的树铸 `<rev>-dirty-<8 hex>`,digest 为 `git diff HEAD --binary` 的 SHA-256 前 4 字节(暂存/未暂存改动皆入;未跟踪文件不置 dirty,同 `git describe --dirty`/`diff-index --quiet HEAD` 语义):同内容同 digest,任何改动必改 banner,两个不同脏树不再可能印出同一戳。`RUBASH_BUILD_HASH` 显式钉值不能洗白脏树——钉值由 crate 侧 `option_env!` 直读(rustc dep-info 自带失效跟踪,改钉即重编),build.rs 把 git 派生戳发在独立变量 `RUBASH_GIT_STAMP`、脏半段单发 `RUBASH_GIT_DIRTY_SUFFIX`;钉值只赢修订半段,仓库在场时 `-dirty[-<digest>]` 仍追加。build.
    • 命令替换 pipeline-filter 死链删除(#710):`command_substitution_pipeline_filter` 的 8 条 filter 臂(sort/sed/tr/head/grep/wc/tail/uniq)连同其唯一调用链——`command_substitution_pipeline_output`、`timed_command_substitution_inner` 的 `|` 臂、`command_substitution_pipeline_first_stage`、孤儿助手 `split_pipeline_words`/`echo_raw_output`——自 rubash#117 平凡性 gate 之后不可达:gate 字节白名单拒绝 `|`,能抵达 filter 的替换体不可能含管线;且该链还只住在 `time` 前缀快捷方式内,进一步收窄。运行时复核:入口探针在 producer×filter 电池(含反引号、嵌套 comsub、alias 注入 `|`、`cat`/generic 首段、`time` 前缀形)上零命中;阳性对照(gate 临时放行 `|`)全部探针命中并复现休眠特征(`$(…|wc)` 默认单列 `1`,而活跃路径是三列)。`$( … | … )` 的可观测行为本就由真执行器管线(pipeline_exec)与外部
  17. oh-my-niu Unreleased unreleased

    Updates

    • Release artifact: `oh-my-niu-2.0.0.zip` (`api = "niubash:[email protected]"`,
    • `min_niubash = "1.0.0"`).
    • Host compatibility: the `git` prompt segment (`[segments.git] id = "vcs"`
    • in `prompts/segments.toml`) now resolves to niubash's native
    • `SegmentId::Vcs` as of niubash 1.4.1 — branch / `branch*` / short SHA,
    • themed `[git_clean]` cyan / `[git_dirty]` yellow, rendered in-process with
    • zero child git processes. No bundle format change: every shipped preset
    • already carries the segment (the `lean` preset is the documented showcase).
    • Renamed the bundle from `oh-my-winuxsh` to `oh-my-niu` to match the
    • niubash 1.0.0 brand. Installed `~/.oh-my-winuxsh` directories keep
    • working: niubash discovers the pre-rename layout as a fallback.
    • Bundle/plugin API identifiers move from the `winuxsh:` prefix to
    • `niubash:` (`niubash:[email protected]`, `niubash:[email protected]`,
    • `niubash:[email protected]`). Manifests may declare either
    • `min_niubash` or the legacy `min_winuxsh` field.
    • Entry point renamed to `oh-my-niu.winux`; scripts now use the `NIU_*`
    • environment prefix (`NIU_PLUGINS`, `NIU_THEME`, `NIU_THEME_PLUGIN`)
    • and the `NIUBASH` bundle-root variable.
    • New first-party plugins: `command-timer` (per-command duration and exit
    • code), `auto-env` (automatic `.venv` activation per directory),
    • `dirmarks` (directory bookmarks), `git-fetch-reminder` (upstream
    • divergence notices), and `env-sync` (push/pull shell config and the WPM
    • package manifest through any rclone remote).
    • All first-party pack manifests move to version 2.0.0 to match the
    • bundle release.