Compare commits

...

23 Commits

Author SHA1 Message Date
Zack Radisic
d2a9e0db54 comments 2025-02-24 14:27:16 -08:00
Zack Radisic
aa777db4b9 Merge branch 2025-02-21 13:29:01 -08:00
pfg
10ae1c4282 more unrecursion progress 2024-11-20 21:01:48 -08:00
pfg
8b2b6fa54c add more tests and mark failing ones as todo 2024-11-20 20:27:34 -08:00
pfg
41ba9f01d0 more conversions to nextexec 2024-11-20 19:54:59 -08:00
pfg
de1ab94e98 Merge branch 'main' into pfg/issue-15189-3 2024-11-20 19:01:44 -08:00
pfg
b43547b4dc the big array doesn't segfault anymore, but there's lots more to convert 2024-11-20 19:01:09 -08:00
pfg
38c41bc9d1 fix '\' handling, still working on ast of "${""}" and ${""} and then have to fix execution of ["echo", "", ""] 2024-11-19 20:59:15 -08:00
pfg
12465ef026 debug log the raw string too 2024-11-19 20:18:28 -08:00
pfg
069b963c73 debug log bun shell command ast 2024-11-19 20:12:17 -08:00
pfg
d714937312 smoe more tests 2024-11-19 19:40:16 -08:00
pfg
8f50ca720e remove the ~ '/' / '\\' logic in interpreter.zig as it's now handled in shell.zig 2024-11-19 19:33:41 -08:00
pfg
1c6c3d971a add failing tests. string is closer to working than substitution but neither work 2024-11-19 19:22:04 -08:00
pfg
ed31a48ebd debug log shell tokens 2024-11-19 19:11:22 -08:00
pfg
0ae03c725a redo ~ expansion & fix it 2024-11-19 19:03:07 -08:00
pfg
18f5b753d6 another test 2024-11-19 17:16:07 -08:00
pfg
fa3c7cdb13 another test 2024-11-19 16:01:51 -08:00
pfg
55a554edc8 another test 2024-11-19 15:58:24 -08:00
pfg
19ceb43643 fix \b__bunstr_0 reading the next number too even though it was unrelated. fix some errors writing to the str pool, returning their memory, and then invalidating it. fix an error panicing. 2024-11-19 15:52:39 -08:00
pfg
cc9b371bab needsEscape(utf16,ascii) -> single unified needsEscape 2024-11-19 15:23:31 -08:00
pfg
128038d896 add more failing tests 2024-11-19 14:39:32 -08:00
pfg
a06d6788d7 don't initialize something to undefined that gets immediately used next loop? 2024-11-18 19:06:08 -08:00
pfg
eb15e135b6 add failing test 2024-11-18 18:38:51 -08:00
6 changed files with 658 additions and 311 deletions

View File

@@ -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, &copy_script, true);
} else {
try copy_script.appendSlice(part);

View File

@@ -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, &copy_script, true);
} else {
try copy_script.appendSlice(part);

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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");

View 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());
});
});