Bun.js: Fixing Escape Sequences In Redirected Output
Are you encountering pesky escape sequences when you redirect the output of your Bun.js scripts to a file? You're not alone! Many developers have run into this issue, especially when trying to log the contents of data structures like Map objects. This article will dive deep into why this happens and, more importantly, how you can effectively resolve it, ensuring your log files are clean, readable, and free from those unsightly [0m and [32m characters. We'll explore the underlying causes, provide practical solutions, and help you get back to enjoying the speed and efficiency of Bun.js without the distraction of corrupted output.
Understanding the Culprit: ANSI Escape Codes and Terminal Output
So, what exactly are these escape sequences that are messing with your redirected output? In the world of command-line interfaces and terminals, these sequences are special character combinations used to control formatting, colors, cursor position, and other features. They are often referred to as ANSI escape codes. When you run a Bun.js script directly in your terminal, the terminal emulator interprets these codes and renders the output accordingly – think colored text, bolding, or even clearing the screen. The console.log function in Bun.js, much like in Node.js, leverages these codes to make your output more visually appealing and informative when viewed interactively.
The problem arises when you redirect this output to a file using the > operator in your shell (like bun index.ts > log). The redirection simply captures the raw text that would have been sent to the terminal. However, unlike your terminal, a plain text file doesn't understand ANSI escape codes. Instead of interpreting them as formatting instructions, it stores them literally as characters within the file. This is why your log file ends up with strings like [0m, [32m, [33m, and [2m interspersed with your actual data. These are the raw representations of the terminal's color and formatting commands. For instance, [32m typically tells the terminal to switch to green text, and [0m usually resets all formatting to default. When redirected, these commands become part of the text content itself, making it unreadable and unprofessional for logging purposes. Understanding this distinction between terminal-rendered output and file-stored output is crucial to solving the problem.
Reproducing the Issue: A Simple Map Example
Let's illustrate this with the exact scenario you provided. We have a simple Bun.js script (index.ts) that creates a Map object and then logs it to the console:
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
console.log(map);
When you run this script directly in your terminal using bun index.ts, Bun.js (and the underlying JavaScript environment) formats the Map object in a human-readable way, often with color coding for keys and values to improve clarity. For example, you might see the string keys in green and the numeric values in yellow, with appropriate escape codes embedded to achieve this effect.
Now, if you redirect this output to a file like so: bun index.ts > log, what happens is that all the characters, including the invisible formatting commands, are sent to the log file. The > operator is a simple stream redirection; it doesn't know or care about the special nature of ANSI escape codes. It just grabs everything that console.log would print and puts it into the file. The result, as you observed, is a file containing the raw Map representation but with all the color and formatting codes explicitly present:
Map(3) {
[0m [32m"a" [0m: [0m [33m1 [0m [0m [2m, [0m
[0m [32m"b" [0m: [0m [33m2 [0m [0m [2m, [0m
[0m [32m"c" [0m: [0m [33m3 [0m [0m [2m, [0m
}
This clearly shows the escape sequences ([0m, [32m, [33m, [2m) that were intended for terminal display now cluttering your log file. The expected behavior, of course, is to have a clean, plain text representation of the Map without any of these control characters, making it suitable for parsing or simple reading.
Solution 1: Disabling Color Output for Redirection
One of the most straightforward ways to tackle the escape sequence problem when redirecting output is to explicitly disable color output when Bun.js detects that its output is being piped or redirected. Many command-line tools and libraries have mechanisms for this, and Bun.js is no exception. The idea is that if the output isn't going to an interactive terminal, there's no need to send color codes, thus preventing them from being written to the log file in the first place.
Bun.js provides a convenient way to check if the standard output (stdout) is a TTY (Teletypewriter, essentially an interactive terminal). You can use process.stdout.isTTY. When this property is false, it means the output is being redirected or piped, and you should adjust your logging behavior. However, directly calling console.log within your script doesn't usually offer a built-in flag to toggle color output based on isTTY. Instead, you might need to use a more robust logging library or manually check this condition if you want fine-grained control.
A common pattern in Node.js and Bun.js applications is to use environment variables or command-line arguments to control features like color. For instance, you could define a convention where if a specific flag is passed (e.g., --no-color or --color=false), color is disabled. Alternatively, you could check process.stdout.isTTY within your application logic and conditionally format your output. However, for simple console.log statements, this approach requires more boilerplate.
A more direct approach, often recommended when dealing with console.log and redirection, is to leverage libraries that are designed with this in mind. For example, libraries like chalk (though primarily for browser and Node.js, concepts apply) or Bun's own $ shell capabilities can sometimes be configured. With Bun's shell capabilities, when you use console.log within a script that is not executed via bun run but directly as a shell command that's being piped, Bun might behave differently. However, the core issue remains: console.log itself, when producing formatted output, embeds these codes.
A practical modification to your existing script would be to create a helper function or use a conditional approach. For instance, you could check process.stdout.isTTY before logging:
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
// Check if stdout is a TTY (interactive terminal)
if (process.stdout.isTTY) {
console.log(map); // Log with colors if interactive
} else {
// Log without colors if redirected
// A simple JSON stringify might work, or a custom formatter
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
}
This if/else block ensures that if process.stdout.isTTY is false (meaning output is being redirected), it falls back to a plain string representation, such as using JSON.stringify. This effectively prevents the ANSI escape codes from ever being generated for non-interactive sessions, thus solving the problem neatly for your log files.
Solution 2: Using JSON.stringify for Clean Output
When escape sequences become a nuisance in your redirected output, especially when dealing with structured data like Map objects, a robust and universally effective solution is to use JSON.stringify. This method converts your JavaScript data structures into a clean, standard JSON string format, which by nature does not include any terminal-specific formatting or color codes. This makes it ideal for logging, data interchange, or any situation where you need plain, predictable text output.
For a Map object, directly applying JSON.stringify(map) won't produce the desired result because JSON.stringify doesn't inherently know how to serialize a Map. However, you can easily convert a Map into a format that JSON.stringify can handle. The most common way is to convert it into an array of key-value pairs or an object. An array of key-value pairs is often the most direct translation for a Map.
You can achieve this conversion using Array.from(map.entries()) or, more concisely, [...map.entries()]. This will give you an array like [['a', 1], ['b', 2], ['c', 3]]. If you prefer an object structure where keys are mapped to values, you can use Object.fromEntries(map) which directly converts the map into an object { a: 1, b: 2, c: 3 }. This object format is also easily stringified.
Let's apply this to your example. Instead of console.log(map), you would use:
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
// Convert Map to an object and then stringify for clean output
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
Here, Object.fromEntries(map) converts the Map into a plain JavaScript object. Then, JSON.stringify serializes this object into a JSON string. The null, 2 arguments are optional but highly recommended for logging; they ensure the JSON output is