mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
23 Commits
claude/str
...
pfg/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a9e0db54 | ||
|
|
aa777db4b9 | ||
|
|
10ae1c4282 | ||
|
|
8b2b6fa54c | ||
|
|
41ba9f01d0 | ||
|
|
de1ab94e98 | ||
|
|
b43547b4dc | ||
|
|
38c41bc9d1 | ||
|
|
12465ef026 | ||
|
|
069b963c73 | ||
|
|
d714937312 | ||
|
|
8f50ca720e | ||
|
|
1c6c3d971a | ||
|
|
ed31a48ebd | ||
|
|
0ae03c725a | ||
|
|
18f5b753d6 | ||
|
|
fa3c7cdb13 | ||
|
|
55a554edc8 | ||
|
|
19ceb43643 | ||
|
|
cc9b371bab | ||
|
|
128038d896 | ||
|
|
a06d6788d7 | ||
|
|
eb15e135b6 |
@@ -495,7 +495,7 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn {
|
||||
|
||||
for (ctx.passthrough) |part| {
|
||||
try copy_script.append(' ');
|
||||
if (bun.shell.needsEscapeUtf8AsciiLatin1(part)) {
|
||||
if (bun.shell.needsEscape(u8, part)) {
|
||||
try bun.shell.escape8Bit(part, ©_script, true);
|
||||
} else {
|
||||
try copy_script.appendSlice(part);
|
||||
|
||||
@@ -285,7 +285,7 @@ pub const RunCommand = struct {
|
||||
|
||||
for (passthrough) |part| {
|
||||
try copy_script.append(' ');
|
||||
if (bun.shell.needsEscapeUtf8AsciiLatin1(part)) {
|
||||
if (bun.shell.needsEscape(u8, part)) {
|
||||
try bun.shell.escape8Bit(part, ©_script, true);
|
||||
} else {
|
||||
try copy_script.appendSlice(part);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -176,7 +176,8 @@ fn setEnv(name: [*:0]const u8, value: [*:0]const u8) void {
|
||||
/// [1] => write end
|
||||
pub const Pipe = [2]bun.FileDescriptor;
|
||||
|
||||
const log = bun.Output.scoped(.SHELL, true);
|
||||
const logscope = bun.Output.Scoped(.SHELL, true);
|
||||
const log = logscope.log;
|
||||
const logsys = bun.Output.scoped(.SYS, true);
|
||||
|
||||
pub const GlobalJS = struct {
|
||||
@@ -1652,6 +1653,14 @@ pub const Parser = struct {
|
||||
break;
|
||||
}
|
||||
},
|
||||
.Tilde => {
|
||||
_ = self.expect(.Tilde);
|
||||
try atoms.append(.tilde);
|
||||
if (next_delimits) {
|
||||
_ = self.match(.Delimit);
|
||||
break;
|
||||
}
|
||||
},
|
||||
.BraceBegin => {
|
||||
has_brace_open = true;
|
||||
_ = self.expect(.BraceBegin);
|
||||
@@ -1700,16 +1709,8 @@ pub const Parser = struct {
|
||||
},
|
||||
.SingleQuotedText, .DoubleQuotedText, .Text => |txtrng| {
|
||||
_ = self.advance();
|
||||
var txt = self.text(txtrng);
|
||||
if (peeked == .Text and txt.len > 0 and txt[0] == '~') {
|
||||
txt = txt[1..];
|
||||
try atoms.append(.tilde);
|
||||
if (txt.len > 0) {
|
||||
try atoms.append(.{ .Text = txt });
|
||||
}
|
||||
} else {
|
||||
try atoms.append(.{ .Text = txt });
|
||||
}
|
||||
const txt = self.text(txtrng);
|
||||
try atoms.append(.{ .Text = txt });
|
||||
if (next_delimits) {
|
||||
_ = self.match(.Delimit);
|
||||
if (should_break) break;
|
||||
@@ -1992,6 +1993,7 @@ pub const TokenTag = enum {
|
||||
Dollar,
|
||||
Asterisk,
|
||||
DoubleAsterisk,
|
||||
Tilde,
|
||||
Eq,
|
||||
Semicolon,
|
||||
Newline,
|
||||
@@ -2033,6 +2035,8 @@ pub const Token = union(TokenTag) {
|
||||
// `*`
|
||||
Asterisk,
|
||||
DoubleAsterisk,
|
||||
// `~`
|
||||
Tilde,
|
||||
|
||||
/// =
|
||||
Eq,
|
||||
@@ -2100,6 +2104,7 @@ pub const Token = union(TokenTag) {
|
||||
.Dollar => "`$`",
|
||||
.Asterisk => "`*`",
|
||||
.DoubleAsterisk => "`**`",
|
||||
.Tilde => "`~`",
|
||||
.Eq => "`+`",
|
||||
.Semicolon => "`;`",
|
||||
.Newline => "`\\n`",
|
||||
@@ -2139,15 +2144,15 @@ pub const LexResult = struct {
|
||||
const size = size: {
|
||||
var i: usize = 0;
|
||||
for (errors) |e| {
|
||||
i += e.msg.len;
|
||||
i += e.msg.end - e.msg.start;
|
||||
}
|
||||
break :size i;
|
||||
};
|
||||
var buf = arena.alloc(u8, size) catch bun.outOfMemory();
|
||||
var i: usize = 0;
|
||||
for (errors) |e| {
|
||||
@memcpy(buf[i .. i + e.msg.len], e.msg);
|
||||
i += e.msg.len;
|
||||
@memcpy(buf[i .. i + (e.msg.end - e.msg.start)], this.strpool[e.msg.start..e.msg.end]);
|
||||
i += e.msg.end - e.msg.start;
|
||||
}
|
||||
break :str buf;
|
||||
};
|
||||
@@ -2155,8 +2160,8 @@ pub const LexResult = struct {
|
||||
}
|
||||
};
|
||||
pub const LexError = struct {
|
||||
/// Allocated with lexer arena
|
||||
msg: []const u8,
|
||||
/// Within str pool
|
||||
msg: struct { start: usize, end: usize },
|
||||
};
|
||||
|
||||
/// A special char used to denote the beginning of a special token
|
||||
@@ -2243,7 +2248,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
const start = self.strpool.items.len;
|
||||
self.strpool.appendSlice(msg) catch bun.outOfMemory();
|
||||
const end = self.strpool.items.len;
|
||||
self.errors.append(.{ .msg = self.strpool.items[start..end] }) catch bun.outOfMemory();
|
||||
self.errors.append(.{ .msg = .{ .start = start, .end = end } }) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
fn make_sublexer(self: *@This(), kind: SubShellKind) @This() {
|
||||
@@ -2439,6 +2444,26 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
continue;
|
||||
},
|
||||
|
||||
// home directory
|
||||
'~' => {
|
||||
comptime assertSpecialChar('~');
|
||||
|
||||
if (self.chars.state == .Single or self.chars.state == .Double) break :escaped;
|
||||
if (self.chars.prev != null and (self.chars.prev.?.char != ' ' or self.chars.prev.?.escaped)) break :escaped;
|
||||
// expand for:
|
||||
// ~/, ~\\, not ~\"
|
||||
if (self.peek()) |next| {
|
||||
if (next.char == '/' or next.char == '\\' or next.char == ' ') {
|
||||
// allowed
|
||||
} else {
|
||||
// not allowed
|
||||
break :escaped;
|
||||
}
|
||||
}
|
||||
try self.break_word(false);
|
||||
try self.tokens.append(.Tilde);
|
||||
},
|
||||
|
||||
// brace expansion syntax
|
||||
'{' => {
|
||||
comptime assertSpecialChar('{');
|
||||
@@ -2686,14 +2711,11 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Treat newline preceded by backslash as whitespace
|
||||
// Ignore newline preceeded by backslash
|
||||
else if (char == '\n') {
|
||||
if (comptime bun.Environment.allow_assert) {
|
||||
assert(input.escaped);
|
||||
}
|
||||
if (self.chars.state != .Double) {
|
||||
try self.break_word_impl(true, true, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2746,7 +2768,9 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
}
|
||||
|
||||
inline fn isImmediatelyEscapedQuote(self: *@This()) bool {
|
||||
return (self.chars.state == .Double and
|
||||
// this doesn't work and probably can't? maybe try to revamp string breaking?
|
||||
// if this is going to be modified, it needs to support bunstr, single quoted strings, and bunstr inside strings
|
||||
return ((self.chars.state == .Double or self.chars.state == .Single) and
|
||||
(self.chars.current != null and !self.chars.current.?.escaped and self.chars.current.?.char == '"') and
|
||||
(self.chars.prev != null and !self.chars.prev.?.escaped and self.chars.prev.?.char == '"'));
|
||||
}
|
||||
@@ -2780,6 +2804,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
.BraceEnd,
|
||||
.CmdSubstEnd,
|
||||
.Asterisk,
|
||||
.Tilde,
|
||||
=> true,
|
||||
|
||||
.Pipe,
|
||||
@@ -3138,17 +3163,24 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
switch (bytes[i]) {
|
||||
'0'...'9' => {
|
||||
if (digit_buf_count >= digit_buf.len) {
|
||||
const ERROR_STR = "Invalid " ++ name ++ " (number too high): ";
|
||||
var error_buf: [ERROR_STR.len + digit_buf.len + 1]u8 = undefined;
|
||||
const error_msg = std.fmt.bufPrint(error_buf[0..], "{s} {s}{c}", .{ ERROR_STR, digit_buf[0..digit_buf_count], bytes[i] }) catch @panic("Should not happen");
|
||||
self.add_error(error_msg);
|
||||
self.add_error("Invalid " ++ name ++ " (number too high)");
|
||||
return null;
|
||||
}
|
||||
digit_buf[digit_buf_count] = bytes[i];
|
||||
digit_buf_count += 1;
|
||||
},
|
||||
else => break,
|
||||
'.' => {
|
||||
i += 1;
|
||||
break;
|
||||
},
|
||||
else => {
|
||||
self.add_error("Invalid " ++ name ++ " (missing '.' at end)");
|
||||
return null;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
self.add_error("Invalid " ++ name ++ " (missing '.' at end)");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (digit_buf_count == 0) {
|
||||
@@ -3157,15 +3189,11 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
}
|
||||
|
||||
const idx = std.fmt.parseInt(usize, digit_buf[0..digit_buf_count], 10) catch {
|
||||
self.add_error("Invalid " ++ name ++ " ref ");
|
||||
self.add_error("Invalid " ++ name ++ " (out of bounds)");
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!validate(self, idx)) return null;
|
||||
// if (idx >= self.string_refs.len) {
|
||||
// self.add_error("Invalid " ++ name ++ " (out of bounds");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// Bump the cursor
|
||||
const new_idx = self.chars.cursorPos() + i;
|
||||
@@ -3193,7 +3221,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
|
||||
|
||||
fn validateJSStringRefIdx(self: *@This(), idx: usize) bool {
|
||||
if (idx >= self.string_refs.len) {
|
||||
self.add_error("Invalid JS string ref (out of bounds");
|
||||
self.add_error("Invalid JS string ref (out of bounds)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3487,31 +3515,37 @@ pub fn ShellCharIter(comptime encoding: StringEncoding) type {
|
||||
}
|
||||
|
||||
pub fn read_char(self: *@This()) ?InputChar {
|
||||
const indexed_value = self.src.index() orelse return null;
|
||||
var char = indexed_value.char;
|
||||
if (char != '\\' or self.state == .Single) return .{ .char = char };
|
||||
while (true) {
|
||||
const indexed_value = self.src.index() orelse return null;
|
||||
var char = indexed_value.char;
|
||||
if (char != '\\' or self.state == .Single) return .{ .char = char };
|
||||
|
||||
// Handle backslash
|
||||
switch (self.state) {
|
||||
.Normal => {
|
||||
const peeked = self.src.indexNext() orelse return null;
|
||||
char = peeked.char;
|
||||
},
|
||||
.Double => {
|
||||
const peeked = self.src.indexNext() orelse return null;
|
||||
switch (peeked.char) {
|
||||
// Backslash only applies to these characters
|
||||
'$', '`', '"', '\\', '\n', '#' => {
|
||||
char = peeked.char;
|
||||
},
|
||||
else => return .{ .char = char, .escaped = false },
|
||||
}
|
||||
},
|
||||
// We checked `self.state == .Single` above so this is impossible
|
||||
.Single => unreachable,
|
||||
// Handle backslash
|
||||
const peeked = self.src.indexNext() orelse return null;
|
||||
if (peeked.char == '\n') {
|
||||
// completely ignore backslash newline, don't advance self.prev/self.current
|
||||
self.src.eat(true);
|
||||
continue;
|
||||
}
|
||||
switch (self.state) {
|
||||
.Normal => {
|
||||
char = peeked.char;
|
||||
},
|
||||
.Double => {
|
||||
switch (peeked.char) {
|
||||
// Backslash only applies to these characters
|
||||
'$', '`', '"', '\\', '#' => {
|
||||
char = peeked.char;
|
||||
},
|
||||
else => return .{ .char = char, .escaped = false },
|
||||
}
|
||||
},
|
||||
// We checked `self.state == .Single` above so this is impossible
|
||||
.Single => unreachable,
|
||||
}
|
||||
|
||||
return .{ .char = char, .escaped = true };
|
||||
}
|
||||
|
||||
return .{ .char = char, .escaped = true };
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3677,6 +3711,8 @@ pub const Test = struct {
|
||||
// *
|
||||
Asterisk,
|
||||
DoubleAsterisk,
|
||||
// ~
|
||||
Tilde,
|
||||
// =
|
||||
Eq,
|
||||
Semicolon,
|
||||
@@ -3714,6 +3750,7 @@ pub const Test = struct {
|
||||
.JSObjRef => |val| return .{ .JSObjRef = val },
|
||||
.Pipe => return .Pipe,
|
||||
.DoublePipe => return .DoublePipe,
|
||||
.Tilde => return .Tilde,
|
||||
.Ampersand => return .Ampersand,
|
||||
.DoubleAmpersand => return .DoubleAmpersand,
|
||||
.Redirect => |r| return .{ .Redirect = r },
|
||||
@@ -3942,7 +3979,7 @@ pub const ShellSrcBuilder = struct {
|
||||
const invalid = bun.simdutf.validate.utf8(utf8);
|
||||
if (!invalid) return false;
|
||||
if (allow_escape) {
|
||||
if (needsEscapeUtf8AsciiLatin1(utf8)) {
|
||||
if (needsEscape(u8, utf8)) {
|
||||
const bunstr = bun.String.createUTF8(utf8);
|
||||
defer bunstr.deref();
|
||||
try this.appendJSStrRef(bunstr);
|
||||
@@ -3976,7 +4013,7 @@ pub const ShellSrcBuilder = struct {
|
||||
|
||||
pub fn appendJSStrRef(this: *ShellSrcBuilder, bunstr: bun.String) bun.OOM!void {
|
||||
const idx = this.jsstrs_to_escape.items.len;
|
||||
const str = std.fmt.bufPrint(this.jsstr_ref_buf[0..], "{s}{d}", .{ LEX_JS_STRING_PREFIX, idx }) catch {
|
||||
const str = std.fmt.bufPrint(this.jsstr_ref_buf[0..], "{s}{d}.", .{ LEX_JS_STRING_PREFIX, idx }) catch {
|
||||
@panic("Impossible");
|
||||
};
|
||||
try this.outbuf.appendSlice(str);
|
||||
@@ -4067,26 +4104,20 @@ pub fn escapeUtf16(str: []const u16, outbuf: *std.ArrayList(u8), comptime add_qu
|
||||
}
|
||||
|
||||
pub fn needsEscapeBunstr(bunstr: bun.String) bool {
|
||||
if (bunstr.isUTF16()) return needsEscapeUTF16(bunstr.utf16());
|
||||
if (bunstr.isUTF16()) return needsEscape(u16, bunstr.utf16());
|
||||
// Otherwise is utf-8, ascii, or latin-1
|
||||
return needsEscapeUtf8AsciiLatin1(bunstr.byteSlice());
|
||||
}
|
||||
|
||||
pub fn needsEscapeUTF16(str: []const u16) bool {
|
||||
for (str) |codeunit| {
|
||||
if (codeunit < 0xff and SPECIAL_CHARS_TABLE.isSet(codeunit)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return needsEscape(u8, bunstr.byteSlice());
|
||||
}
|
||||
|
||||
/// For ascii, latin-1, utf-8, or utf-16.
|
||||
/// Checks for the presence of any char from `SPECIAL_CHARS` in `str`. This
|
||||
/// indicates the *possibility* that the string must be escaped, so it can have
|
||||
/// false positives, but it is faster than running the shell lexer through the
|
||||
/// input string for a more correct implementation.
|
||||
pub fn needsEscapeUtf8AsciiLatin1(str: []const u8) bool {
|
||||
pub fn needsEscape(comptime backing_char: type, str: []const backing_char) bool {
|
||||
if (str.len == 0) return true;
|
||||
for (str) |c| {
|
||||
if (SPECIAL_CHARS_TABLE.isSet(c)) return true;
|
||||
if (c <= 0xFF and SPECIAL_CHARS_TABLE.isSet(c)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,15 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type {
|
||||
|
||||
break :brk args;
|
||||
};
|
||||
return @call(.auto, func, args);
|
||||
const ret = @call(.auto, func, args);
|
||||
if (Ret == void and @TypeOf(ret) == @import("shell/interpreter.zig").Interpreter.NextExec) {
|
||||
ret.executeTodoReview();
|
||||
return {};
|
||||
}
|
||||
if (Ret == @import("shell/interpreter.zig").Interpreter.NextExec and @TypeOf(ret) == void) {
|
||||
return .todo_review;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@panic("Invalid tag");
|
||||
|
||||
200
test/regression/issue/15189.test.ts
Normal file
200
test/regression/issue/15189.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { $ } from "bun";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
|
||||
// consider porting bash tests: https://github.com/bminor/bash/tree/f3b6bd19457e260b65d11f2712ec3da56cef463f/tests
|
||||
// they're not too hard - add as .sh files, execute them with bun, and expect the results to be the same as the .right files
|
||||
|
||||
// TODO benchmark:
|
||||
// before & after: ``await $`echo ${Array(10000).fill("a")}`;``
|
||||
|
||||
describe("bun shell", () => {
|
||||
it("does not segfault 1", async () => {
|
||||
expect(await $`echo ${Array(1000000).fill("a")}`.text()).toBe(Array(1000000).fill("a").join(" ") + "\n");
|
||||
});
|
||||
it("does not segfault 2", async () => {
|
||||
expect(await $({ raw: ["echo" + " a".repeat(1000000)] } as any).text()).toBe(
|
||||
Array(1000000).fill("a").join(" ") + "\n",
|
||||
);
|
||||
});
|
||||
it("does not segfault 3", async () => {
|
||||
// slow
|
||||
expect(await $({ raw: ["echo " + 'a"a"'.repeat(1000000)] } as any).text()).toBe("aa".repeat(1000000) + "\n");
|
||||
});
|
||||
it.todo("echo works 1", async () => expect(await $`echo -n`.text()).toBe(""));
|
||||
it.todo("echo works 2", async () => expect(await $`echo -n abc`.text()).toBe("abc"));
|
||||
it("echo works 3", async () => expect(await $`echo abc`.text()).toBe("abc\n"));
|
||||
it("echo works 4", async () => expect(await $`echo`.text()).toBe("\n"));
|
||||
it("echo works 5", async () => expect(await $`echo abc def`.text()).toBe("abc def\n"));
|
||||
it.todo("echo works 6", async () => expect(await $`echo -s abc def`.text()).toBe("abcdef\n"));
|
||||
it.todo("echo works 7", async () => expect(await $`echo -E abc def`.text()).toBe("abc def\n"));
|
||||
it.todo("echo works 8", async () => expect(await $`echo abc\ndef`.text()).toBe("abc\\ndef\n"));
|
||||
it.todo("echo works 9", async () => expect(await $`echo -e abc\ndef`.text()).toBe("abc\ndef\n"));
|
||||
it.todo("passes correct number of arguments with empty string substitutions", async () => {
|
||||
expect(await $`echo 1 ${""} 2`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty string substitutions 2", async () => {
|
||||
expect(await $`echo 1 "${""}" 2`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty string substitutions 3", async () => {
|
||||
expect(await $`echo 1 '${""}' 2`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty strings", async () => {
|
||||
expect(await $`echo 1 $(echo -n) 2`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty strings 2", async () => {
|
||||
expect(await $`echo 1 $(echo) 2`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty strings 3", async () => {
|
||||
expect(await $`echo 1 $(echo -n 3) 2`.text()).toBe("1 3 2\n");
|
||||
});
|
||||
it("passes correct number of arguments with empty strings 4", async () => {
|
||||
expect(await $`echo 1 $(echo 3) 2`.text()).toBe("1 3 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty double string quotes", async () => {
|
||||
expect(await $`echo "1" "" "2"`.text()).toBe("1 2\n");
|
||||
});
|
||||
it.todo("passes correct number of arguments with empty single string quotes", async () => {
|
||||
expect(await $`echo '1' '' '2'`.text()).toBe("1 2\n");
|
||||
});
|
||||
it("doesn't cause invalid js string ref error with a number after a string ref", async () => {
|
||||
expect(await $`echo ${'"'}1`.text()).toBe('"1\n');
|
||||
});
|
||||
it("does not crash with an invalid string ref 1", async () => {
|
||||
expect(() => $`echo __bunstr_123.`.text()).toThrowError("Invalid JS string ref (out of bounds)");
|
||||
});
|
||||
it("does not crash with an invalid string ref 2", async () => {
|
||||
expect(() => $`echo __bunstr_123`.text()).toThrowError("Invalid JS string ref (missing '.' at end)");
|
||||
});
|
||||
it("does not crash with an invalid string ref 3", async () => {
|
||||
expect(() => $`echo __bunstr_123456789012345678901234567890.`.text()).toThrowError(
|
||||
"Invalid JS string ref (out of bounds)",
|
||||
);
|
||||
});
|
||||
it("does not crash with an invalid string ref 4", async () => {
|
||||
expect(() => $`echo __bunstr_123456789012345678901234567890123.`.text()).toThrowError(
|
||||
"Invalid JS string ref (number too high)",
|
||||
);
|
||||
});
|
||||
it("does not crash with an invalid string ref 5", async () => {
|
||||
expect(() => $`echo __bunstr_123a`.text()).toThrowError("Invalid JS string ref (missing '.' at end)");
|
||||
});
|
||||
it("does not crash with an invalid string ref 6", async () => {
|
||||
expect(await $`echo ${'"'} __bunstr_0.`.text()).toBe('" "\n');
|
||||
});
|
||||
it("doesn't parse string refs inside substitution", async () => {
|
||||
expect(await $`echo ${"\x08__bunstr_123."}`.text()).toBe("\x08__bunstr_123.\n");
|
||||
});
|
||||
it("does not expand tilde in ${}", async () => {
|
||||
expect(await $`echo ${"~"}`.text()).toBe("~\n");
|
||||
});
|
||||
it("does not expand tilde when escaped", async () => {
|
||||
expect(await $`echo \~`.text()).toBe("~\n");
|
||||
});
|
||||
it("does not expand tilde when the slash is quoted", async () => {
|
||||
expect(await $`echo ~"/"`.text()).toBe("~/\n");
|
||||
});
|
||||
it("does not expand tilde when there's an empty string between", async () => {
|
||||
expect(await $`echo ~""/`.text()).toBe("~/\n");
|
||||
});
|
||||
it("expands tilde", async () => {
|
||||
expect(await $`echo ~`.text()).toBe(process.env.HOME + "\n");
|
||||
});
|
||||
it("expands with slash", async () => {
|
||||
expect(await $`echo ~/`.text()).toBe(process.env.HOME + "/\n");
|
||||
});
|
||||
it("does not expand after escaped space", async () => {
|
||||
expect(await $`echo \ ~`.text()).toBe(" ~\n");
|
||||
});
|
||||
it("expands tilde as middle argument", async () => {
|
||||
expect(await $`echo a ~ b`.text()).toBe("a " + process.env.HOME + " b\n");
|
||||
});
|
||||
it("expands tilde as middle argument 2", async () => {
|
||||
expect(
|
||||
await $`echo a ~\
|
||||
b`.text(),
|
||||
).toBe("a " + process.env.HOME + " b\n");
|
||||
});
|
||||
it("expands as first argument", async () => {
|
||||
expect((await $`~`.nothrow()).exitCode).not.toBe(0);
|
||||
});
|
||||
it("does not expand tilde with a non-slash after", async () => {
|
||||
expect(await $`echo ~~`.text()).toBe("~~\n");
|
||||
});
|
||||
it("allow tilde expansion with backslash", async () => {
|
||||
expect(await $`echo ~\\a`.text()).toBe(process.env.HOME + "\\a\n");
|
||||
});
|
||||
it("does not allow tilde expansion with non-backslash backslash", async () => {
|
||||
expect(await $`echo ~\"a`.text()).toBe('~"a\n');
|
||||
});
|
||||
it("does not expand tilde with a non-slash after", async () => {
|
||||
expect(await $`echo ~{a,b}`.text()).toBe("~a ~b\n");
|
||||
});
|
||||
it("does not expand tilde when the tilde is quoted", async () => {
|
||||
expect(await $`echo "~"`.text()).toBe("~\n");
|
||||
});
|
||||
it("does not expand tilde after equals", async () => {
|
||||
// expect(await $`echo --home=~`.text()).toBe("--home=" + process.env.HOME + "\n"); // bash feature, not in sh, zsh, csh, or fish
|
||||
expect(await $`echo --home=~`.text()).toBe("--home=~\n"); // bash feature, not in sh
|
||||
});
|
||||
it("does not expand tilde after colon", async () => {
|
||||
expect(await $`echo a:~`.text()).toBe("a:~\n");
|
||||
});
|
||||
it.todo("expands tilde in variable set", async () => {
|
||||
expect(await $`MYVAR=~/abc && echo $MYVAR`.text()).toBe(process.env.HOME + "/abc\n");
|
||||
});
|
||||
it.todo("expands tilde in variable set list", async () => {
|
||||
expect(await $`MYVAR=a:~:b && echo $MYVAR`.text()).toBe("a:" + process.env.HOME + ":b\n");
|
||||
});
|
||||
it("does not expand tilde in variable set list with non-split char", async () => {
|
||||
expect(await $`MYVAR=a:~c:b && echo $MYVAR`.text()).toBe("a:~c:b\n");
|
||||
});
|
||||
it("does not expand tilde in variable set list with quotes", async () => {
|
||||
expect(await $`MYVAR=a:"~":b && echo $MYVAR`.text()).toBe("a:~:b\n");
|
||||
});
|
||||
it("does not expand tilde second", async () => {
|
||||
expect(await $`echo "a"~`.text()).toBe("a~\n");
|
||||
});
|
||||
it("handles backslashed newline", async () => {
|
||||
expect(
|
||||
await $`echo a\
|
||||
b`.text(),
|
||||
).toBe("ab\n");
|
||||
});
|
||||
it("handles backslashed newline in single quotes", async () => {
|
||||
expect(
|
||||
await $`echo 'a\
|
||||
b'`.text(),
|
||||
).toBe("a\\\nb\n");
|
||||
});
|
||||
it("handles backslashed newline in double quotes", async () => {
|
||||
expect(
|
||||
await $`echo "a\
|
||||
b"`.text(),
|
||||
).toBe("ab\n");
|
||||
});
|
||||
// TODO: handle username (`~user` -> getpwnam(user) eg /home/user if the accont exists. but only if all unquoted, ie `~user"a"` <- not allowed)
|
||||
|
||||
it("fails for bad surrogate pairs", async () => {
|
||||
expect(() => $`echo ${"😊".substring(0, 1)}`.text()).toThrowError("Shell script string contains invalid UTF-16");
|
||||
expect(() => $`echo ${"😊".substring(1, 2)}`.text()).toThrowError("Shell script string contains invalid UTF-16");
|
||||
expect(await $`echo ${"😊".substring(0, 2)}`.text()).toBe("😊\n");
|
||||
});
|
||||
|
||||
const this_filename = import.meta.dirname + "/15189.test.ts";
|
||||
it("works with files", async () => {
|
||||
expect(await $`cat ${this_filename} | cat | cat | cat`.text()).toBe(await Bun.file(this_filename).text());
|
||||
});
|
||||
it("works with files 2", async () => {
|
||||
expect(await $`cat < ${Bun.file(this_filename)} | cat | cat | cat`.text()).toBe(
|
||||
await Bun.file(this_filename).text(),
|
||||
);
|
||||
});
|
||||
it("works with files 3", async () => {
|
||||
expect(await $`cat < ${this_filename} | cat | cat | cat`.text()).toBe(await Bun.file(this_filename).text());
|
||||
});
|
||||
it("works with files 4", async () => {
|
||||
const tmpdir = tempDirWithFiles("works-with-files", {});
|
||||
await $`cat < ${this_filename} | cat | cat | cat > ${tmpdir}/outfile.txt`;
|
||||
expect(await Bun.file(tmpdir + "/outfile.txt").text()).toBe(await Bun.file(this_filename).text());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user