Skip to content

Commit 0d760e4

Browse files
committed
export-and-alias
1 parent 33c8044 commit 0d760e4

File tree

7 files changed

+340
-12
lines changed

7 files changed

+340
-12
lines changed

src/commands/alias/alias.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ describe("alias command", () => {
4848
});
4949
});
5050

51+
// Note: Alias expansion is NOT implemented to match real bash behavior.
52+
// In non-interactive mode (scripts), bash does not expand aliases.
53+
// The alias command only stores/lists alias definitions.
54+
5155
describe("unalias command", () => {
5256
it("should remove an alias", async () => {
5357
const env = new BashEnv();

src/commands/date/date.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { describe, expect, it } from "vitest";
22
import { BashEnv } from "../../BashEnv.js";
33

4+
/** Format date in local timezone as YYYY-MM-DD */
5+
function formatLocalDate(date: Date): string {
6+
const year = date.getFullYear();
7+
const month = String(date.getMonth() + 1).padStart(2, "0");
8+
const day = String(date.getDate()).padStart(2, "0");
9+
return `${year}-${month}-${day}`;
10+
}
11+
412
describe("date", () => {
513
describe("format specifiers", () => {
614
it("should format year with %Y", async () => {
@@ -186,28 +194,27 @@ describe("date", () => {
186194
it("should parse 'today'", async () => {
187195
const env = new BashEnv();
188196
const result = await env.exec("date -d today +%F");
189-
const today = new Date().toISOString().slice(0, 10);
197+
// Use local date formatting to match date command behavior
198+
const today = formatLocalDate(new Date());
190199
expect(result.stdout).toBe(`${today}\n`);
191200
expect(result.exitCode).toBe(0);
192201
});
193202

194203
it("should parse 'yesterday'", async () => {
195204
const env = new BashEnv();
196205
const result = await env.exec("date -d yesterday +%F");
197-
const yesterday = new Date(Date.now() - 86400000)
198-
.toISOString()
199-
.slice(0, 10);
200-
expect(result.stdout).toBe(`${yesterday}\n`);
206+
const yesterday = new Date();
207+
yesterday.setDate(yesterday.getDate() - 1);
208+
expect(result.stdout).toBe(`${formatLocalDate(yesterday)}\n`);
201209
expect(result.exitCode).toBe(0);
202210
});
203211

204212
it("should parse 'tomorrow'", async () => {
205213
const env = new BashEnv();
206214
const result = await env.exec("date -d tomorrow +%F");
207-
const tomorrow = new Date(Date.now() + 86400000)
208-
.toISOString()
209-
.slice(0, 10);
210-
expect(result.stdout).toBe(`${tomorrow}\n`);
215+
const tomorrow = new Date();
216+
tomorrow.setDate(tomorrow.getDate() + 1);
217+
expect(result.stdout).toBe(`${formatLocalDate(tomorrow)}\n`);
211218
expect(result.exitCode).toBe(0);
212219
});
213220
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { afterEach, beforeEach, describe, it } from "vitest";
2+
import {
3+
cleanupTestDir,
4+
compareOutputs,
5+
createTestDir,
6+
setupFiles,
7+
} from "./test-helpers.js";
8+
9+
describe("alias command - Real Bash Comparison", () => {
10+
let testDir: string;
11+
12+
beforeEach(async () => {
13+
testDir = await createTestDir();
14+
});
15+
16+
afterEach(async () => {
17+
await cleanupTestDir(testDir);
18+
});
19+
20+
// Note: Alias expansion is not implemented in bash-env to match real bash behavior.
21+
// In non-interactive mode (scripts), bash does not expand aliases.
22+
23+
describe("alias management", () => {
24+
it("should show alias not found error", async () => {
25+
const env = await setupFiles(testDir, {});
26+
await compareOutputs(env, testDir, "alias notexists || echo failed");
27+
});
28+
});
29+
30+
describe("unalias", () => {
31+
it("should remove an alias", async () => {
32+
const env = await setupFiles(testDir, {});
33+
await compareOutputs(
34+
env,
35+
testDir,
36+
"alias greet='echo hi'; unalias greet; alias greet || echo removed",
37+
);
38+
});
39+
40+
it("should error when unaliasing non-existent alias", async () => {
41+
const env = await setupFiles(testDir, {});
42+
await compareOutputs(
43+
env,
44+
testDir,
45+
"unalias nonexistent || echo not_found",
46+
);
47+
});
48+
});
49+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { afterEach, beforeEach, describe, it } from "vitest";
2+
import {
3+
cleanupTestDir,
4+
compareOutputs,
5+
createTestDir,
6+
setupFiles,
7+
} from "./test-helpers.js";
8+
9+
describe("export command - Real Bash Comparison", () => {
10+
let testDir: string;
11+
12+
beforeEach(async () => {
13+
testDir = await createTestDir();
14+
});
15+
16+
afterEach(async () => {
17+
await cleanupTestDir(testDir);
18+
});
19+
20+
describe("setting variables", () => {
21+
it("should set and use a variable", async () => {
22+
const env = await setupFiles(testDir, {});
23+
await compareOutputs(env, testDir, "export FOO=bar; echo $FOO");
24+
});
25+
26+
it("should set multiple variables", async () => {
27+
const env = await setupFiles(testDir, {});
28+
await compareOutputs(
29+
env,
30+
testDir,
31+
"export A=1 B=2 C=3; echo $A $B $C",
32+
);
33+
});
34+
35+
it("should handle value with equals sign", async () => {
36+
const env = await setupFiles(testDir, {});
37+
await compareOutputs(
38+
env,
39+
testDir,
40+
"export URL='http://x.com?a=1'; echo $URL",
41+
);
42+
});
43+
44+
it("should handle empty value", async () => {
45+
const env = await setupFiles(testDir, {});
46+
await compareOutputs(env, testDir, "export EMPTY=; echo \"[$EMPTY]\"");
47+
});
48+
});
49+
50+
describe("variable usage", () => {
51+
it("should be available in subshell", async () => {
52+
const env = await setupFiles(testDir, {});
53+
await compareOutputs(env, testDir, "export FOO=bar; (echo $FOO)");
54+
});
55+
56+
it("should work with test command", async () => {
57+
const env = await setupFiles(testDir, {});
58+
await compareOutputs(
59+
env,
60+
testDir,
61+
'export VAL=yes; [ "$VAL" = "yes" ] && echo matched',
62+
);
63+
});
64+
65+
it("should work with numeric comparison", async () => {
66+
const env = await setupFiles(testDir, {});
67+
await compareOutputs(
68+
env,
69+
testDir,
70+
"export NUM=10; [ $NUM -gt 5 ] && echo greater",
71+
);
72+
});
73+
74+
it("should work in string interpolation", async () => {
75+
const env = await setupFiles(testDir, {});
76+
await compareOutputs(
77+
env,
78+
testDir,
79+
"export NAME=world; echo \"hello $NAME\"",
80+
);
81+
});
82+
});
83+
84+
describe("inline export", () => {
85+
it("should allow setting and using in same line", async () => {
86+
const env = await setupFiles(testDir, {});
87+
await compareOutputs(
88+
env,
89+
testDir,
90+
"export X=42 && echo $X",
91+
);
92+
});
93+
});
94+
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("export builtin", () => {
5+
describe("setting variables", () => {
6+
it("should set a variable with export NAME=value", async () => {
7+
const env = new BashEnv();
8+
await env.exec("export FOO=bar");
9+
const result = await env.exec("echo $FOO");
10+
expect(result.stdout).toBe("bar\n");
11+
});
12+
13+
it("should set multiple variables", async () => {
14+
const env = new BashEnv();
15+
await env.exec("export FOO=bar BAZ=qux");
16+
const result = await env.exec("echo $FOO $BAZ");
17+
expect(result.stdout).toBe("bar qux\n");
18+
});
19+
20+
it("should handle value with equals sign", async () => {
21+
const env = new BashEnv();
22+
await env.exec("export URL=http://example.com?foo=bar");
23+
const result = await env.exec("echo $URL");
24+
expect(result.stdout).toBe("http://example.com?foo=bar\n");
25+
});
26+
27+
it("should create empty variable when NAME has no value", async () => {
28+
const env = new BashEnv();
29+
await env.exec("export EMPTY");
30+
const result = await env.exec('test -z "$EMPTY" && echo empty');
31+
expect(result.stdout).toBe("empty\n");
32+
});
33+
34+
it("should preserve existing variable value with export NAME", async () => {
35+
const env = new BashEnv({ env: { EXISTING: "value" } });
36+
await env.exec("export EXISTING");
37+
const result = await env.exec("echo $EXISTING");
38+
expect(result.stdout).toBe("value\n");
39+
});
40+
});
41+
42+
describe("listing variables", () => {
43+
it("should list all exported variables with no args", async () => {
44+
const env = new BashEnv({ env: { FOO: "bar", BAZ: "qux" } });
45+
const result = await env.exec("export");
46+
expect(result.stdout).toContain("declare -x FOO='bar'");
47+
expect(result.stdout).toContain("declare -x BAZ='qux'");
48+
});
49+
50+
it("should list all exported variables with -p", async () => {
51+
const env = new BashEnv({ env: { FOO: "bar" } });
52+
const result = await env.exec("export -p");
53+
expect(result.stdout).toContain("declare -x FOO='bar'");
54+
});
55+
56+
it("should escape single quotes in values", async () => {
57+
const env = new BashEnv();
58+
await env.exec("export MSG=\"it's working\"");
59+
const result = await env.exec("export");
60+
expect(result.stdout).toContain("it'\\''s working");
61+
});
62+
63+
it("should not list aliases", async () => {
64+
const env = new BashEnv();
65+
await env.exec("alias ll='ls -la'");
66+
await env.exec("export FOO=bar");
67+
const result = await env.exec("export");
68+
expect(result.stdout).not.toContain("BASH_ALIAS");
69+
expect(result.stdout).toContain("FOO");
70+
});
71+
});
72+
73+
describe("un-exporting with -n", () => {
74+
it("should remove variable with -n", async () => {
75+
const env = new BashEnv({ env: { FOO: "bar" } });
76+
await env.exec("export -n FOO");
77+
const result = await env.exec('test -z "$FOO" && echo removed');
78+
expect(result.stdout).toBe("removed\n");
79+
});
80+
81+
it("should remove multiple variables with -n", async () => {
82+
const env = new BashEnv({ env: { FOO: "bar", BAZ: "qux" } });
83+
await env.exec("export -n FOO BAZ");
84+
const result = await env.exec(
85+
'test -z "$FOO" && test -z "$BAZ" && echo removed',
86+
);
87+
expect(result.stdout).toBe("removed\n");
88+
});
89+
});
90+
91+
describe("variable usage", () => {
92+
it("exported variable should be available in command", async () => {
93+
const env = new BashEnv();
94+
await env.exec("export GREETING=hello");
95+
const result = await env.exec("echo $GREETING world");
96+
expect(result.stdout).toBe("hello world\n");
97+
});
98+
99+
it("exported variable should be available in subshell", async () => {
100+
const env = new BashEnv();
101+
await env.exec("export FOO=bar");
102+
const result = await env.exec("(echo $FOO)");
103+
expect(result.stdout).toBe("bar\n");
104+
});
105+
106+
it("should work with conditional", async () => {
107+
const env = new BashEnv();
108+
await env.exec("export DEBUG=1");
109+
const result = await env.exec('[ "$DEBUG" = "1" ] && echo debug_on');
110+
expect(result.stdout).toBe("debug_on\n");
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)