Skip to content

Commit 7e4fc07

Browse files
authored
fix(macos): brace $code to stop unbound-variable crash in menu bar apply alert (#254)
fix(macos): brace $code to stop unbound-variable crash in menu bar apply failure alert (#251) apply-from-menubar-macos.sh sources common-macos.sh's `set -euo pipefail`, which leaves `-u` active even after the later `set +e`. The failure-path alert referenced a bare $code immediately followed by a full-width right parenthesis (`$code)`); under a UTF-8 locale, bash misparses that as a different, never-assigned identifier and aborts with "unbound variable" instead of showing the real failure — even though `code=$?` had assigned it correctly a few lines earlier. Reproduced against the real common-macos.sh under LANG=en_US.UTF-8: bare `$code)` crashes, `${code})` does not. Braces disambiguate the token boundary. Add a repo-wide static regression test (mirroring the existing no-nested-:has() CSS guard) that fails on any bare $var immediately followed by CJK punctuation in a shell script, so this class of bug can't silently return. This means every failure of `start-dream-skin-macos.sh --restart-existing` from the menu bar — including, but not limited to, the unrelated native-window verification issue — was showing this bogus crash instead of the actual error message.
1 parent b5a614e commit 7e4fc07

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

macos/scripts/apply-from-menubar-macos.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,6 @@ if [ "$code" -eq 0 ]; then
122122
fi
123123

124124
detail="$(/usr/bin/tail -n 5 "$LOG_OUT" 2>/dev/null | /usr/bin/tr '\n' ' ' | /usr/bin/cut -c1-350)"
125-
alert "应用失败($code)。$detail"
125+
alert "应用失败(${code})。$detail"
126126
progress "应用失败"
127127
exit "$code"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { readFileSync, readdirSync } from "node:fs";
4+
import { dirname, join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
// A bare $var immediately followed by full-width CJK punctuation (no braces)
8+
// can make bash misparse the variable name boundary under a UTF-8 locale
9+
// with `set -u` active, raising "unbound variable" even though the variable
10+
// is assigned — masking the real error behind a bogus one. Reproduced with
11+
// the real common-macos.sh under LANG=en_US.UTF-8: `$code)` crashes,
12+
// `${code})` does not (#251). Require braces whenever a bare $var expansion
13+
// directly abuts CJK punctuation.
14+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
15+
const scriptDirs = ["scripts", "menubar"];
16+
const cjkPunctuation = ")。,:;!?」』】";
17+
const bareVarBeforeCjk = new RegExp(`\\$[A-Za-z_][A-Za-z0-9_]*[${cjkPunctuation}]`, "g");
18+
19+
const listShFiles = (dir) => {
20+
const files = [];
21+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
22+
const path = join(dir, entry.name);
23+
if (entry.isDirectory()) files.push(...listShFiles(path));
24+
else if (entry.name.endsWith(".sh")) files.push(path);
25+
}
26+
return files;
27+
};
28+
29+
const files = scriptDirs.flatMap((dir) => listShFiles(join(root, dir)));
30+
assert.ok(files.length > 0, "expected to find at least one shell script to scan");
31+
32+
for (const file of files) {
33+
test(`no bare $var immediately before CJK punctuation in ${file.slice(root.length + 1)}`, () => {
34+
const source = readFileSync(file, "utf8");
35+
const findings = [...source.matchAll(bareVarBeforeCjk)].map((match) => match[0]);
36+
assert.deepEqual(findings, [], `bare $var before CJK punctuation found in ${file}`);
37+
});
38+
}

0 commit comments

Comments
 (0)